mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-29 09:49:04 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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 \"设置中允许共享。";
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ private func uniqueCombine(_ fileName: String, fullPath: Bool = false) -> String
|
||||
let name = ns.deletingPathExtension
|
||||
let ext = ns.pathExtension
|
||||
let suffix = (n == 0) ? "" : "_\(n)"
|
||||
let f = "\(name)\(suffix).\(ext)"
|
||||
let f = ext.isEmpty ? "\(name)\(suffix)" : "\(name)\(suffix).\(ext)"
|
||||
return (FileManager.default.fileExists(atPath: fullPath ? f : getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
|
||||
}
|
||||
return tryCombine(fileName, 0)
|
||||
|
||||
@@ -1659,7 +1659,7 @@ alert button */
|
||||
"Develop" = "Разработване";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Инструменти за разработчици";
|
||||
"Developer" = "Инструменти за разработчици";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Устройство";
|
||||
@@ -3217,7 +3217,7 @@ alert button */
|
||||
"Preview" = "Визуализация";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Поверителност и сигурност";
|
||||
"Your privacy" = "Поверителност и сигурност";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Private filenames" = "Поверителни имена на файлове";
|
||||
@@ -4452,7 +4452,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Можете да активирате по-късно през Настройки";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението.";
|
||||
"You can enable them later via app Your privacy settings." = "Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Можете да опитате още веднъж.";
|
||||
|
||||
@@ -1306,7 +1306,7 @@ alert button */
|
||||
"Develop" = "Vyvinout";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Nástroje pro vývojáře";
|
||||
"Developer" = "Nástroje pro vývojáře";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Zařízení";
|
||||
@@ -2578,7 +2578,7 @@ alert button */
|
||||
"Preview" = "Náhled";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Ochrana osobních údajů a zabezpečení";
|
||||
"Your privacy" = "Ochrana osobních údajů a zabezpečení";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Private filenames" = "Soukromé názvy souborů";
|
||||
@@ -3543,7 +3543,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Můžete povolit později v Nastavení";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Můžete je povolit později v nastavení Soukromí & Bezpečnosti aplikace";
|
||||
"You can enable them later via app Your privacy settings." = "Můžete je povolit později v nastavení Soukromí & Bezpečnosti aplikace";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can hide or mute a user profile - swipe it to the right." = "Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava.";
|
||||
|
||||
@@ -2064,7 +2064,7 @@ alert button */
|
||||
"Developer options" = "Optionen für Entwickler";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Entwicklertools";
|
||||
"Developer" = "Entwicklertools";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Gerät";
|
||||
@@ -4533,7 +4533,7 @@ alert button */
|
||||
"Previously connected servers" = "Bisher verbundene Server";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Datenschutz & Sicherheit";
|
||||
"Your privacy" = "Datenschutz & Sicherheit";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "Schutz der Privatsphäre Ihrer Kunden.";
|
||||
@@ -6759,7 +6759,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Sie können diese später in den Einstellungen aktivieren";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Sie können diese später in den Datenschutz & Sicherheits-Einstellungen der App aktivieren.";
|
||||
"You can enable them later via app Your privacy settings." = "Sie können diese später in den Datenschutz & Sicherheits-Einstellungen der App aktivieren.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Sie können es nochmal probieren.";
|
||||
|
||||
@@ -2064,7 +2064,7 @@ alert button */
|
||||
"Developer options" = "Opciones desarrollador";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Herramientas desarrollo";
|
||||
"Developer" = "Herramientas desarrollo";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Dispositivo";
|
||||
@@ -4533,7 +4533,7 @@ alert button */
|
||||
"Previously connected servers" = "Servidores conectados previamente";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Seguridad y Privacidad";
|
||||
"Your privacy" = "Seguridad y Privacidad";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "Privacidad para tus clientes.";
|
||||
@@ -6759,7 +6759,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Puedes activar más tarde en Configuración";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Puedes activarlos más tarde en la configuración de Privacidad y Seguridad.";
|
||||
"You can enable them later via app Your privacy settings." = "Puedes activarlos más tarde en la configuración de Privacidad y Seguridad.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Puedes intentarlo de nuevo.";
|
||||
|
||||
@@ -982,7 +982,7 @@ alert button */
|
||||
"Develop" = "Kehitä";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Kehittäjätyökalut";
|
||||
"Developer" = "Kehittäjätyökalut";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Laite";
|
||||
@@ -2232,7 +2232,7 @@ new chat action */
|
||||
"Preview" = "Esikatselu";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Yksityisyys ja turvallisuus";
|
||||
"Your privacy" = "Yksityisyys ja turvallisuus";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Private filenames" = "Yksityiset tiedostonimet";
|
||||
@@ -3179,7 +3179,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Voit ottaa käyttöön myöhemmin asetusten kautta";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista.";
|
||||
"You can enable them later via app Your privacy settings." = "Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can hide or mute a user profile - swipe it to the right." = "Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle.";
|
||||
|
||||
@@ -1745,7 +1745,7 @@ alert button */
|
||||
"Developer options" = "Options pour les développeurs";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Outils du développeur";
|
||||
"Developer" = "Outils du développeur";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Appareil";
|
||||
@@ -3742,7 +3742,7 @@ alert button */
|
||||
"Previously connected servers" = "Serveurs précédemment connectés";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Vie privée et sécurité";
|
||||
"Your privacy" = "Vie privée et sécurité";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "Respect de la vie privée de vos clients.";
|
||||
@@ -5448,7 +5448,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Vous pouvez l'activer ultérieurement via Paramètres";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Vous pouvez les activer ultérieurement via les paramètres de Confidentialité et Sécurité de l'application.";
|
||||
"You can enable them later via app Your privacy settings." = "Vous pouvez les activer ultérieurement via les paramètres de Confidentialité et Sécurité de l'application.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Vous pouvez faire un nouvel essai.";
|
||||
|
||||
@@ -2064,7 +2064,7 @@ alert button */
|
||||
"Developer options" = "Fejlesztői beállítások";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Fejlesztői eszközök";
|
||||
"Developer" = "Fejlesztői eszközök";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Eszköz";
|
||||
@@ -4533,7 +4533,7 @@ alert button */
|
||||
"Previously connected servers" = "Korábban kapcsolódott kiszolgálók";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Adatvédelem és biztonság";
|
||||
"Your privacy" = "Adatvédelem és biztonság";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "Saját ügyfeleinek adatvédelme.";
|
||||
@@ -6759,7 +6759,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Később engedélyezheti a beállításokban";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az „Adatvédelem és biztonság” menüben.";
|
||||
"You can enable them later via app Your privacy settings." = "Később engedélyezheti őket az „Adatvédelem és biztonság” menüben.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Megpróbálhatja még egyszer.";
|
||||
|
||||
@@ -2064,7 +2064,7 @@ alert button */
|
||||
"Developer options" = "Opzioni sviluppatore";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Strumenti di sviluppo";
|
||||
"Developer" = "Strumenti di sviluppo";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Dispositivo";
|
||||
@@ -4533,7 +4533,7 @@ alert button */
|
||||
"Previously connected servers" = "Server precedentemente connessi";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Privacy e sicurezza";
|
||||
"Your privacy" = "Privacy e sicurezza";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "Privacy per i tuoi clienti.";
|
||||
@@ -6759,7 +6759,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Puoi attivarle più tardi nelle impostazioni";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Puoi attivarle più tardi nelle impostazioni di privacy e sicurezza dell'app.";
|
||||
"You can enable them later via app Your privacy settings." = "Puoi attivarle più tardi nelle impostazioni di privacy e sicurezza dell'app.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Puoi fare un altro tentativo.";
|
||||
|
||||
@@ -1270,7 +1270,7 @@ alert button */
|
||||
"Developer options" = "開発者向けの設定";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "開発ツール";
|
||||
"Developer" = "開発ツール";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "端末";
|
||||
@@ -2533,7 +2533,7 @@ alert button */
|
||||
"Preview" = "プレビュー";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "プライバシーとセキュリティ";
|
||||
"Your privacy" = "プライバシーとセキュリティ";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Private filenames" = "プライベートなファイル名";
|
||||
@@ -3459,7 +3459,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "あとで設定から有効にできます";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。";
|
||||
"You can enable them later via app Your privacy settings." = "あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can hide or mute a user profile - swipe it to the right." = "ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。";
|
||||
|
||||
@@ -1773,7 +1773,7 @@ alert button */
|
||||
"Developer options" = "Ontwikkelaars opties";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Ontwikkel gereedschap";
|
||||
"Developer" = "Ontwikkel gereedschap";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Apparaat";
|
||||
@@ -3929,7 +3929,7 @@ alert button */
|
||||
"Previously connected servers" = "Eerder verbonden servers";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Privacy en beveiliging";
|
||||
"Your privacy" = "Privacy en beveiliging";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "Privacy voor uw klanten.";
|
||||
@@ -5777,7 +5777,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "U kunt later inschakelen via Instellingen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "U kunt ze later inschakelen via de privacy- en beveiligingsinstellingen van de app.";
|
||||
"You can enable them later via app Your privacy settings." = "U kunt ze later inschakelen via de privacy- en beveiligingsinstellingen van de app.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Je kunt het nog een keer proberen.";
|
||||
|
||||
@@ -1845,7 +1845,7 @@ alert button */
|
||||
"Developer options" = "Opcje deweloperskie";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Narzędzia deweloperskie";
|
||||
"Developer" = "Narzędzia deweloperskie";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Urządzenie";
|
||||
@@ -4131,7 +4131,7 @@ alert button */
|
||||
"Previously connected servers" = "Wcześniej połączone serwery";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Prywatność i bezpieczeństwo";
|
||||
"Your privacy" = "Prywatność i bezpieczeństwo";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "Prywatność dla Twoich klientów.";
|
||||
@@ -6135,7 +6135,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Możesz włączyć później w Ustawieniach";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Możesz je włączyć później w ustawieniach Prywatności i Bezpieczeństwa aplikacji.";
|
||||
"You can enable them later via app Your privacy settings." = "Możesz je włączyć później w ustawieniach Prywatności i Bezpieczeństwa aplikacji.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Możesz spróbować ponownie.";
|
||||
|
||||
@@ -101,7 +101,7 @@ End-to-end encrypted audio and video communication.
|
||||
| Call history | Call events displayed as chat items | `Shared/Views/Chat/ChatItem/CICallItemView.swift` |
|
||||
| Incoming call view | Dedicated UI for incoming call notifications | `Shared/Views/Call/IncomingCallView.swift` |
|
||||
|
||||
### 5. Privacy & Security
|
||||
### 5. Your privacy
|
||||
|
||||
Encryption, authentication, and privacy controls.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
Configure all aspects of app behavior including notifications, network/servers, privacy, appearance, database management, call settings, and developer tools. Accessed from the UserPicker sheet on the chat list.
|
||||
Configure all aspects of app behavior including notifications, network/servers, privacy, appearance, database management, call settings, and Developer. Accessed from the UserPicker sheet on the chat list.
|
||||
|
||||
## Route / Navigation
|
||||
|
||||
@@ -22,7 +22,7 @@ Configure all aspects of app behavior including notifications, network/servers,
|
||||
| Notifications | `bolt` (color varies by token status) | `NotificationsView` | Push notification mode and preview settings |
|
||||
| Network & servers | `externaldrive.connected.to.line.below` | `NetworkAndServers` | SMP/XFTP servers, proxy, .onion hosts, advanced network |
|
||||
| Audio & video calls | `video` | `CallSettings` | WebRTC relay policy, ICE servers, CallKit options |
|
||||
| Privacy & security | `lock` | `PrivacySettings` | SimpleX Lock, screen protection, delivery receipts, auto-accept |
|
||||
| Your privacy | `lock` | `PrivacySettings` | SimpleX Lock, screen protection, delivery receipts, auto-accept |
|
||||
| Appearance | `sun.max` | `AppearanceSettings` | Theme, language, wallpapers, chat bubbles, toolbar opacity |
|
||||
|
||||
All rows disabled when `chatModel.chatRunning != true`. Appearance row only shown when `UIApplication.shared.supportsAlternateIcons`.
|
||||
@@ -77,7 +77,7 @@ Adding a relay: `NewChatRelayView` form with name, address, test, and enable tog
|
||||
|
||||
Server validation (`validateServers_`) now returns both errors and warnings.
|
||||
|
||||
#### Privacy & Security (`PrivacySettings`)
|
||||
#### Your privacy (`PrivacySettings`)
|
||||
|
||||
| Setting | Description |
|
||||
|---|---|
|
||||
@@ -152,7 +152,7 @@ Database row shows exclamation octagon icon in red when `chatRunning == false`.
|
||||
|
||||
| Row | Icon | Destination | Description |
|
||||
|---|---|---|---|
|
||||
| Developer tools | `chevron.left.forwardslash.chevron.right` | `DeveloperView` | Chat console/terminal, log level, confirm DB upgrades |
|
||||
| Developer | `chevron.left.forwardslash.chevron.right` | `DeveloperView` | Chat console/terminal, log level, confirm DB upgrades |
|
||||
| App version | (none) | `VersionView` | Shows "v{version} ({build})" |
|
||||
|
||||
## Loading / Error States
|
||||
|
||||
@@ -2064,7 +2064,7 @@ alert button */
|
||||
"Developer options" = "Опции разработчика";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Инструменты разработчика";
|
||||
"Developer" = "Разработчик";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Устройство";
|
||||
@@ -4533,7 +4533,7 @@ alert button */
|
||||
"Previously connected servers" = "Ранее подключенные серверы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Конфиденциальность";
|
||||
"Your privacy" = "Конфиденциальность";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "Конфиденциальность для ваших покупателей.";
|
||||
@@ -6759,7 +6759,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Вы можете включить их позже в Настройках";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Вы можете включить их позже в настройках Конфиденциальности.";
|
||||
"You can enable them later via app Your privacy settings." = "Вы можете включить их позже в настройках Конфиденциальности.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Вы можете попробовать ещё раз.";
|
||||
|
||||
@@ -946,7 +946,7 @@ alert button */
|
||||
"Develop" = "พัฒนา";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "เครื่องมือสำหรับนักพัฒนา";
|
||||
"Developer" = "เครื่องมือสำหรับนักพัฒนา";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "อุปกรณ์";
|
||||
@@ -2172,7 +2172,7 @@ new chat action */
|
||||
"Preview" = "ดูตัวอย่าง";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "ความเป็นส่วนตัวและความปลอดภัย";
|
||||
"Your privacy" = "ความเป็นส่วนตัวและความปลอดภัย";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Private filenames" = "ชื่อไฟล์ส่วนตัว";
|
||||
@@ -3089,7 +3089,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "คุณสามารถเปิดใช้งานในภายหลังผ่านการตั้งค่า";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป";
|
||||
"You can enable them later via app Your privacy settings." = "คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can hide or mute a user profile - swipe it to the right." = "คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา";
|
||||
|
||||
@@ -1859,7 +1859,7 @@ alert button */
|
||||
"Developer options" = "Geliştirici seçenekleri";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Geliştirici araçları";
|
||||
"Developer" = "Geliştirici araçları";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Cihaz";
|
||||
@@ -4099,7 +4099,7 @@ alert button */
|
||||
"Previously connected servers" = "Önceden bağlanılmış sunucular";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Gizlilik & güvenlik";
|
||||
"Your privacy" = "Gizlilik & güvenlik";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "Müşterileriniz için gizlilik.";
|
||||
@@ -6064,7 +6064,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Daha sonra Ayarlardan etkinleştirebilirsin";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Daha sonra uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz.";
|
||||
"You can enable them later via app Your privacy settings." = "Daha sonra uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Bir kez daha deneyebilirsiniz.";
|
||||
|
||||
@@ -1806,7 +1806,7 @@ alert button */
|
||||
"Developer options" = "Можливості для розробників";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "Інструменти для розробників";
|
||||
"Developer" = "Інструменти для розробників";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "Пристрій";
|
||||
@@ -4019,7 +4019,7 @@ alert button */
|
||||
"Previously connected servers" = "Раніше підключені сервери";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "Конфіденційність і безпека";
|
||||
"Your privacy" = "Конфіденційність і безпека";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "Конфіденційність для ваших клієнтів.";
|
||||
@@ -5966,7 +5966,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "Ви можете увімкнути пізніше в Налаштуваннях";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми.";
|
||||
"You can enable them later via app Your privacy settings." = "Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "Ви можете спробувати ще раз.";
|
||||
|
||||
@@ -1836,7 +1836,7 @@ alert button */
|
||||
"Developer options" = "开发者选项";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Developer tools" = "开发者工具";
|
||||
"Developer" = "开发者工具";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Device" = "设备";
|
||||
@@ -4107,7 +4107,7 @@ alert button */
|
||||
"Previously connected servers" = "以前连接的服务器";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "隐私和安全";
|
||||
"Your privacy" = "隐私和安全";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy for your customers." = "客户隐私。";
|
||||
@@ -6093,7 +6093,7 @@ server test failure */
|
||||
"You can enable later via Settings" = "您可以稍后在设置中启用它";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can enable them later via app Privacy & Security settings." = "您可以稍后通过应用程序的 \"隐私与安全 \"设置启用它们。";
|
||||
"You can enable them later via app Your privacy settings." = "您可以稍后通过应用程序的 \"隐私与安全 \"设置启用它们。";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can give another try." = "你可以再试一次。";
|
||||
|
||||
+36
-11
@@ -1,7 +1,15 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -11,19 +19,19 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
actual fun SettingsSectionApp(
|
||||
actual fun AdvancedSettingsAppSection(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_app)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp)
|
||||
SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) })
|
||||
SectionView {
|
||||
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) })
|
||||
AppVersionItem(showVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun AppShutdownItem() {
|
||||
SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), ::shutdownAppAlert)
|
||||
}
|
||||
|
||||
fun restartApp() {
|
||||
ProcessPhoenix.triggerRebirth(androidAppContext)
|
||||
@@ -36,11 +44,28 @@ private fun shutdownApp() {
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
|
||||
private fun shutdownAppAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
private fun shutdownAppAlert() {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.shutdown_alert_question),
|
||||
text = generalGetString(MR.strings.shutdown_alert_desc),
|
||||
destructive = true,
|
||||
onConfirm = onConfirm
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({ AlertManager.shared.hideAlert() }) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
restartApp()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.settings_restart_app), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
shutdownApp()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.settings_shutdown), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+10
-4
@@ -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,
|
||||
|
||||
+10
-4
@@ -288,16 +288,22 @@ expect fun AttachmentSelection(
|
||||
)
|
||||
|
||||
fun MutableState<ComposeState>.onFilesAttached(uris: List<URI>) {
|
||||
val groups = uris.groupBy { isImage(it) }
|
||||
val images = groups[true] ?: emptyList()
|
||||
val groups = uris.groupBy { isImage(it) || isVideoUri(it) }
|
||||
val media = groups[true] ?: emptyList()
|
||||
val files = groups[false] ?: emptyList()
|
||||
if (images.isNotEmpty()) {
|
||||
CoroutineScope(Dispatchers.IO).launch { processPickedMedia(images, null) }
|
||||
if (media.isNotEmpty()) {
|
||||
CoroutineScope(Dispatchers.IO).launch { processPickedMedia(media, null) }
|
||||
} else if (files.isNotEmpty()) {
|
||||
processPickedFile(uris.first(), null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isVideoUri(uri: URI): Boolean {
|
||||
val name = getFileName(uri)?.lowercase() ?: return false
|
||||
return name.endsWith(".mov") || name.endsWith(".avi") || name.endsWith(".mp4") ||
|
||||
name.endsWith(".mpg") || name.endsWith(".mpeg") || name.endsWith(".mkv")
|
||||
}
|
||||
|
||||
fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) {
|
||||
if (uri != null) {
|
||||
val fileSize = getFileSize(uri)
|
||||
|
||||
+4
-2
@@ -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) {
|
||||
|
||||
+20
-5
@@ -12,8 +12,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.*
|
||||
@@ -1224,12 +1226,25 @@ fun Modifier.clipChatItem(chatItem: ChatItem? = null, tailVisible: Boolean = fal
|
||||
val style = shapeStyle(chatItem, chatItemTail.value, tailVisible, revealed)
|
||||
val cornerRoundness = chatItemRoundness.value.coerceIn(0f, 1f)
|
||||
|
||||
val shape = when (style) {
|
||||
is ShapeStyle.Bubble -> chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true)
|
||||
is ShapeStyle.RoundRect -> RoundedCornerShape(style.radius * cornerRoundness)
|
||||
return when (style) {
|
||||
is ShapeStyle.Bubble -> {
|
||||
// Modifier.clip of the bubble GenericShape mis-hit-tests its path on very tall
|
||||
// items, dropping long-press on the lower part of the bubble (issue #6991). Clip
|
||||
// in the draw pass instead — drawing is clipped identically (the press ripple
|
||||
// included), with no effect on hit-test.
|
||||
val shape = chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true)
|
||||
this.drawWithCache {
|
||||
val path = Path().apply {
|
||||
addOutline(shape.createOutline(size, layoutDirection, this@drawWithCache))
|
||||
}
|
||||
onDrawWithContent {
|
||||
clipPath(path) { this@onDrawWithContent.drawContent() }
|
||||
}
|
||||
}
|
||||
}
|
||||
// RoundRect hit-tests correctly — no bug here, keep the antialiased Modifier.clip.
|
||||
is ShapeStyle.RoundRect -> this.clip(RoundedCornerShape(style.radius * cornerRoundness))
|
||||
}
|
||||
|
||||
return this.clip(shape)
|
||||
}
|
||||
|
||||
private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boolean, sent: Boolean = false): GenericShape = GenericShape { size, _ ->
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+158
-124
@@ -44,29 +44,8 @@ fun DatabaseView() {
|
||||
val prefs = m.controller.appPrefs
|
||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) }
|
||||
val chatArchiveFile = remember { mutableStateOf<String?>(null) }
|
||||
val stopped = remember { m.chatRunning }.value == false
|
||||
val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? ->
|
||||
val archive = chatArchiveFile.value
|
||||
if (archive != null && to != null) {
|
||||
copyFileToFile(File(archive), to) {}
|
||||
}
|
||||
// delete no matter the database was exported or canceled the export process
|
||||
if (archive != null) {
|
||||
File(archive).delete()
|
||||
chatArchiveFile.value = null
|
||||
}
|
||||
}
|
||||
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) }
|
||||
val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||
if (to != null) {
|
||||
importArchiveAlert {
|
||||
stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) {
|
||||
importArchive(to, appFilesCountAndSize, progressIndicator, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val chatItemTTL = remember { mutableStateOf(m.chatItemTTL.value) }
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
@@ -79,27 +58,10 @@ fun DatabaseView() {
|
||||
useKeychain.value,
|
||||
m.chatDbEncrypted.value,
|
||||
m.controller.appPrefs.storeDBPassphrase.state.value,
|
||||
m.controller.appPrefs.initialRandomDBPassphrase,
|
||||
importArchiveLauncher,
|
||||
appFilesCountAndSize,
|
||||
chatItemTTL,
|
||||
user,
|
||||
m.users,
|
||||
startChat = { startChat(m, chatLastStart, m.chatDbChanged, progressIndicator) },
|
||||
stopChatAlert = { stopChatAlert(m, progressIndicator) },
|
||||
exportArchive = {
|
||||
stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) {
|
||||
exportArchive(m, progressIndicator, chatArchiveFile, saveArchiveLauncher)
|
||||
}
|
||||
},
|
||||
deleteChatAlert = {
|
||||
deleteChatAlert {
|
||||
stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) {
|
||||
deleteChat(m, progressIndicator)
|
||||
true
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteAppFilesAndMedia = {
|
||||
deleteFilesAndMediaAlert {
|
||||
stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) {
|
||||
@@ -120,12 +82,9 @@ fun DatabaseView() {
|
||||
setCiTTL(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize)
|
||||
}
|
||||
},
|
||||
disconnectAllHosts = {
|
||||
val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected }
|
||||
connected.forEachIndexed { index, h ->
|
||||
controller.stopRemoteHostAndReloadHosts(h, index == connected.lastIndex && chatModel.connectedToRemote())
|
||||
}
|
||||
}
|
||||
showDatabaseManagement = {
|
||||
ModalManager.start.showModal(cardScreen = true) { DatabaseManagementView() }
|
||||
},
|
||||
)
|
||||
if (progressIndicator.value) {
|
||||
Box(
|
||||
@@ -151,24 +110,18 @@ fun DatabaseLayout(
|
||||
useKeyChain: Boolean,
|
||||
chatDbEncrypted: Boolean?,
|
||||
passphraseSaved: Boolean,
|
||||
initialRandomDBPassphrase: SharedPreference<Boolean>,
|
||||
importArchiveLauncher: FileChooserLauncher,
|
||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||
chatItemTTL: MutableState<ChatItemTTL>,
|
||||
currentUser: User?,
|
||||
users: List<UserInfo>,
|
||||
startChat: () -> Unit,
|
||||
stopChatAlert: () -> Unit,
|
||||
exportArchive: () -> Unit,
|
||||
deleteChatAlert: () -> Unit,
|
||||
deleteAppFilesAndMedia: () -> Unit,
|
||||
onChatItemTTLSelected: (ChatItemTTL?) -> Unit,
|
||||
disconnectAllHosts: () -> Unit,
|
||||
showDatabaseManagement: () -> Unit,
|
||||
) {
|
||||
val operationsDisabled = progressIndicator && !chatModel.desktopNoUserNoRemote
|
||||
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.your_chat_database))
|
||||
AppBarTitle(stringResource(MR.strings.chat_data))
|
||||
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SectionView(stringResource(MR.strings.messages_section_title)) {
|
||||
@@ -187,79 +140,17 @@ fun DatabaseLayout(
|
||||
)
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected }
|
||||
if (chatModel.localUserCreated.value == true) {
|
||||
// still show the toggle in case database was stopped when the user opened this screen because it can be in the following situations:
|
||||
// - database was stopped after migration and the app relaunched
|
||||
// - something wrong happened with database operations and the database couldn't be launched when it should
|
||||
SectionView(stringResource(MR.strings.run_chat_section)) {
|
||||
if (!toggleEnabled) {
|
||||
SectionItemView(disconnectAllHosts) {
|
||||
Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange)
|
||||
}
|
||||
}
|
||||
RunChatSetting(stopped, toggleEnabled && !progressIndicator, startChat, stopChatAlert)
|
||||
}
|
||||
if (stopped) SectionTextFooter(stringResource(MR.strings.you_must_use_the_most_recent_version_of_database))
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
SectionView(stringResource(MR.strings.chat_database_section)) {
|
||||
if (chatModel.localUserCreated.value != true && !toggleEnabled) {
|
||||
SectionItemView(disconnectAllHosts) {
|
||||
Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange)
|
||||
}
|
||||
}
|
||||
SectionView {
|
||||
val unencrypted = chatDbEncrypted == false
|
||||
SettingsActionItem(
|
||||
if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled)
|
||||
else painterResource(MR.images.ic_lock),
|
||||
stringResource(MR.strings.database_passphrase),
|
||||
click = { ModalManager.start.showModal(cardScreen = true) { DatabaseEncryptionView(chatModel, false) } },
|
||||
stringResource(MR.strings.database_passphrase_and_export),
|
||||
click = showDatabaseManagement,
|
||||
iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
if (appPlatform.isDesktop) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_folder_open),
|
||||
stringResource(MR.strings.open_database_folder),
|
||||
::desktopOpenDatabaseDir,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
}
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_ios_share),
|
||||
stringResource(MR.strings.export_database),
|
||||
click = {
|
||||
if (initialRandomDBPassphrase.get()) {
|
||||
exportProhibitedAlert()
|
||||
ModalManager.start.showModal {
|
||||
DatabaseEncryptionView(chatModel, false)
|
||||
}
|
||||
} else {
|
||||
exportArchive()
|
||||
}
|
||||
},
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_download),
|
||||
stringResource(MR.strings.import_database),
|
||||
{ withLongRunningApi { importArchiveLauncher.launch("application/zip") } },
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_delete_forever),
|
||||
stringResource(MR.strings.delete_database),
|
||||
deleteChatAlert,
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
@@ -287,6 +178,155 @@ fun DatabaseLayout(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DatabaseManagementView() {
|
||||
val m = chatModel
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val prefs = m.controller.appPrefs
|
||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) }
|
||||
val chatArchiveFile = remember { mutableStateOf<String?>(null) }
|
||||
val stopped = remember { m.chatRunning }.value == false
|
||||
val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? ->
|
||||
val archive = chatArchiveFile.value
|
||||
if (archive != null && to != null) {
|
||||
copyFileToFile(File(archive), to) {}
|
||||
}
|
||||
// delete no matter the database was exported or canceled the export process
|
||||
if (archive != null) {
|
||||
File(archive).delete()
|
||||
chatArchiveFile.value = null
|
||||
}
|
||||
}
|
||||
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) }
|
||||
val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||
if (to != null) {
|
||||
importArchiveAlert {
|
||||
stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) {
|
||||
importArchive(to, appFilesCountAndSize, progressIndicator, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val operationsDisabled = progressIndicator.value && !m.desktopNoUserNoRemote
|
||||
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.database_passphrase_and_export))
|
||||
|
||||
val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected }
|
||||
val disconnectAllHosts = {
|
||||
val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected }
|
||||
connected.forEachIndexed { index, h ->
|
||||
controller.stopRemoteHostAndReloadHosts(h, index == connected.lastIndex && chatModel.connectedToRemote())
|
||||
}
|
||||
}
|
||||
SectionView(stringResource(MR.strings.chat_database_section)) {
|
||||
if (chatModel.localUserCreated.value != true && !toggleEnabled) {
|
||||
SectionItemView(disconnectAllHosts) {
|
||||
Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange)
|
||||
}
|
||||
}
|
||||
val unencrypted = m.chatDbEncrypted.value == false
|
||||
SettingsActionItem(
|
||||
if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeychain.value) painterResource(MR.images.ic_vpn_key_filled)
|
||||
else painterResource(MR.images.ic_lock),
|
||||
stringResource(MR.strings.database_passphrase),
|
||||
click = { ModalManager.start.showModal(cardScreen = true) { DatabaseEncryptionView(chatModel, false) } },
|
||||
iconColor = if (unencrypted || (appPlatform.isDesktop && prefs.storeDBPassphrase.state.value)) WarningOrange else MaterialTheme.colors.secondary,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
if (appPlatform.isDesktop) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_folder_open),
|
||||
stringResource(MR.strings.open_database_folder),
|
||||
::desktopOpenDatabaseDir,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
}
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_ios_share),
|
||||
stringResource(MR.strings.export_database),
|
||||
click = {
|
||||
if (prefs.initialRandomDBPassphrase.get()) {
|
||||
exportProhibitedAlert()
|
||||
ModalManager.start.showModal {
|
||||
DatabaseEncryptionView(chatModel, false)
|
||||
}
|
||||
} else {
|
||||
stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) {
|
||||
exportArchive(m, progressIndicator, chatArchiveFile, saveArchiveLauncher)
|
||||
}
|
||||
}
|
||||
},
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_download),
|
||||
stringResource(MR.strings.import_database),
|
||||
{ withLongRunningApi { importArchiveLauncher.launch("application/zip") } },
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_delete_forever),
|
||||
stringResource(MR.strings.delete_database),
|
||||
{
|
||||
deleteChatAlert {
|
||||
stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) {
|
||||
deleteChat(m, progressIndicator)
|
||||
true
|
||||
}
|
||||
}
|
||||
},
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
}
|
||||
|
||||
if (chatModel.localUserCreated.value == true) {
|
||||
SectionDividerSpaced()
|
||||
// still show the toggle in case database was stopped when the user opened this screen because it can be in the following situations:
|
||||
// - database was stopped after migration and the app relaunched
|
||||
// - something wrong happened with database operations and the database couldn't be launched when it should
|
||||
SectionView(stringResource(MR.strings.run_chat_section)) {
|
||||
if (!toggleEnabled) {
|
||||
SectionItemView(disconnectAllHosts) {
|
||||
Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange)
|
||||
}
|
||||
}
|
||||
RunChatSetting(
|
||||
stopped,
|
||||
toggleEnabled && !progressIndicator.value,
|
||||
startChat = { startChat(m, chatLastStart, m.chatDbChanged, progressIndicator) },
|
||||
stopChatAlert = { stopChatAlert(m, progressIndicator) }
|
||||
)
|
||||
}
|
||||
if (stopped) SectionTextFooter(stringResource(MR.strings.you_must_use_the_most_recent_version_of_database))
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
if (progressIndicator.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setChatItemTTLAlert(
|
||||
m: ChatModel, rhId: Long?, selectedChatItemTTL: MutableState<ChatItemTTL>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
@@ -832,19 +872,13 @@ fun PreviewDatabaseLayout() {
|
||||
useKeyChain = false,
|
||||
chatDbEncrypted = false,
|
||||
passphraseSaved = false,
|
||||
initialRandomDBPassphrase = SharedPreference({ true }, {}),
|
||||
importArchiveLauncher = rememberFileChooserLauncher(true) {},
|
||||
appFilesCountAndSize = remember { mutableStateOf(0 to 0L) },
|
||||
chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) },
|
||||
currentUser = User.sampleData,
|
||||
users = listOf(UserInfo.sampleData),
|
||||
startChat = {},
|
||||
stopChatAlert = {},
|
||||
exportArchive = {},
|
||||
deleteChatAlert = {},
|
||||
deleteAppFilesAndMedia = {},
|
||||
onChatItemTTLSelected = {},
|
||||
disconnectAllHosts = {},
|
||||
showDatabaseManagement = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-3
@@ -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))
|
||||
)
|
||||
|
||||
+1
-1
@@ -383,7 +383,7 @@ fun uniqueCombine(fileName: String, dir: File): String {
|
||||
val ext = orig.extension
|
||||
fun tryCombine(n: Int): String {
|
||||
val suffix = if (n == 0) "" else "_$n"
|
||||
val f = "$name$suffix.$ext"
|
||||
val f = if (ext.isEmpty()) "$name$suffix" else "$name$suffix.$ext"
|
||||
return if (File(dir, f).exists()) tryCombine(n + 1) else f
|
||||
}
|
||||
return tryCombine(0)
|
||||
|
||||
+4
-27
@@ -24,43 +24,28 @@ import kotlin.collections.ArrayList
|
||||
fun NotificationsSettingsView(
|
||||
chatModel: ChatModel,
|
||||
) {
|
||||
val onNotificationPreviewModeSelected = { mode: NotificationPreviewMode ->
|
||||
chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name)
|
||||
chatModel.notificationPreviewMode.value = mode
|
||||
}
|
||||
|
||||
NotificationsSettingsLayout(
|
||||
notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state },
|
||||
notificationPreviewMode = chatModel.notificationPreviewMode,
|
||||
showPage = { page ->
|
||||
showNotificationsMode = {
|
||||
ModalManager.start.showModalCloseable(true) {
|
||||
when (page) {
|
||||
CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) }
|
||||
CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected)
|
||||
}
|
||||
NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
enum class CurrentPage {
|
||||
NOTIFICATIONS_MODE, NOTIFICATION_PREVIEW_MODE
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationsSettingsLayout(
|
||||
notificationsMode: State<NotificationsMode>,
|
||||
notificationPreviewMode: State<NotificationPreviewMode>,
|
||||
showPage: (CurrentPage) -> Unit,
|
||||
showNotificationsMode: () -> Unit,
|
||||
) {
|
||||
val modes = remember { notificationModes() }
|
||||
val previewModes = remember { notificationPreviewModes() }
|
||||
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.notifications))
|
||||
SectionView(null) {
|
||||
if (appPlatform == AppPlatform.ANDROID) {
|
||||
SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), { showPage(CurrentPage.NOTIFICATIONS_MODE) }) {
|
||||
SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), showNotificationsMode) {
|
||||
Text(
|
||||
modes.firstOrNull { it.value == notificationsMode.value }?.title ?: "",
|
||||
maxLines = 1,
|
||||
@@ -69,14 +54,6 @@ fun NotificationsSettingsLayout(
|
||||
)
|
||||
}
|
||||
}
|
||||
SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notification_preview_mode_title), { showPage(CurrentPage.NOTIFICATION_PREVIEW_MODE) }) {
|
||||
Text(
|
||||
previewModes.firstOrNull { it.value == notificationPreviewMode.value }?.title ?: "",
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) {
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization))
|
||||
|
||||
+102
-55
@@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -73,6 +74,48 @@ fun PrivacySettingsView(
|
||||
stringResource(MR.strings.sanitize_links_toggle),
|
||||
chatModel.controller.appPrefs.privacySanitizeLinks
|
||||
)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_files)) {
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) {
|
||||
appPrefs.privacyMediaBlurRadius.set(it)
|
||||
}
|
||||
}
|
||||
|
||||
val currentUser = chatModel.currentUser.value
|
||||
if (currentUser != null && !chatModel.desktopNoUserNoRemote) {
|
||||
SectionDividerSpaced()
|
||||
ContacRequestsFromGroupsSection(
|
||||
currentUser = currentUser,
|
||||
setAutoAcceptGrpDirectInvs = { enable ->
|
||||
withApi {
|
||||
chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable)
|
||||
chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
SectionView {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_more_horiz),
|
||||
stringResource(MR.strings.more_privacy),
|
||||
showSettingsModal { MorePrivacyView(it) }
|
||||
)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MorePrivacyView(chatModel: ChatModel) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.more_privacy))
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_chats)) {
|
||||
SettingsPreferenceItem(
|
||||
painterResource(MR.images.ic_chat_bubble),
|
||||
stringResource(MR.strings.privacy_show_last_messages),
|
||||
@@ -98,10 +141,6 @@ fun PrivacySettingsView(
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
|
||||
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
|
||||
})
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) {
|
||||
appPrefs.privacyMediaBlurRadius.set(it)
|
||||
}
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays)
|
||||
}
|
||||
SectionTextFooter(
|
||||
@@ -111,9 +150,34 @@ fun PrivacySettingsView(
|
||||
stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers)
|
||||
}
|
||||
)
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.notifications)) {
|
||||
val previewModes = remember { notificationPreviewModes() }
|
||||
val notificationPreviewMode = remember { chatModel.notificationPreviewMode }
|
||||
SettingsActionItemWithContent(
|
||||
painterResource(MR.images.ic_visibility_off),
|
||||
stringResource(MR.strings.settings_notification_preview_mode_title),
|
||||
click = {
|
||||
ModalManager.start.showModalCloseable(true) {
|
||||
NotificationPreviewView(notificationPreviewMode) { mode ->
|
||||
chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name)
|
||||
chatModel.notificationPreviewMode.value = mode
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
previewModes.firstOrNull { it.value == notificationPreviewMode.value }?.title ?: "",
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val currentUser = chatModel.currentUser.value
|
||||
if (currentUser != null) {
|
||||
if (currentUser != null && !chatModel.desktopNoUserNoRemote) {
|
||||
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
|
||||
withLongRunningApi(slow = 60_000) {
|
||||
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
|
||||
@@ -164,57 +228,40 @@ fun PrivacySettingsView(
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoAcceptGrpDirectInvs(enable: Boolean) {
|
||||
withApi {
|
||||
chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable)
|
||||
chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable)
|
||||
SectionDividerSpaced()
|
||||
DeliveryReceiptsSection(
|
||||
currentUser = currentUser,
|
||||
setOrAskSendReceiptsContacts = { enable ->
|
||||
val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts
|
||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||
} else {
|
||||
count
|
||||
}
|
||||
}
|
||||
if (contactReceiptsOverrides == 0) {
|
||||
setSendReceiptsContacts(enable, clearOverrides = false)
|
||||
} else {
|
||||
showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts)
|
||||
}
|
||||
},
|
||||
setOrAskSendReceiptsGroups = { enable ->
|
||||
val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts
|
||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||
} else {
|
||||
count
|
||||
}
|
||||
}
|
||||
if (groupReceiptsOverrides == 0) {
|
||||
setSendReceiptsGroups(enable, clearOverrides = false)
|
||||
} else {
|
||||
showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SectionDividerSpaced()
|
||||
ContacRequestsFromGroupsSection(
|
||||
currentUser = currentUser,
|
||||
setAutoAcceptGrpDirectInvs = { enable ->
|
||||
setAutoAcceptGrpDirectInvs(enable)
|
||||
}
|
||||
)
|
||||
|
||||
SectionDividerSpaced()
|
||||
DeliveryReceiptsSection(
|
||||
currentUser = currentUser,
|
||||
setOrAskSendReceiptsContacts = { enable ->
|
||||
val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts
|
||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||
} else {
|
||||
count
|
||||
}
|
||||
}
|
||||
if (contactReceiptsOverrides == 0) {
|
||||
setSendReceiptsContacts(enable, clearOverrides = false)
|
||||
} else {
|
||||
showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts)
|
||||
}
|
||||
},
|
||||
setOrAskSendReceiptsGroups = { enable ->
|
||||
val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat ->
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts
|
||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||
} else {
|
||||
count
|
||||
}
|
||||
}
|
||||
if (groupReceiptsOverrides == 0) {
|
||||
setSendReceiptsGroups(enable, clearOverrides = false)
|
||||
} else {
|
||||
showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
|
||||
+52
-39
@@ -37,17 +37,15 @@ import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: () -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
val showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) = { modalView -> { ModalManager.start.showModal(settings = true, cardScreen = true) { modalView(chatModel) } } }
|
||||
SettingsLayout(
|
||||
stopped,
|
||||
chatModel.chatDbEncrypted.value == true,
|
||||
remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value,
|
||||
remember { chatModel.controller.appPrefs.notificationsMode.state },
|
||||
user?.displayName,
|
||||
setPerformLA = setPerformLA,
|
||||
showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } },
|
||||
showSettingsModal = { modalView -> { ModalManager.start.showModal(settings = true, cardScreen = true) { modalView(chatModel) } } },
|
||||
showSettingsModal = showSettingsModal,
|
||||
showSettingsModalWithSearch = { modalView ->
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
val search = rememberSaveable { mutableStateOf("") }
|
||||
@@ -62,12 +60,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: (
|
||||
},
|
||||
showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } },
|
||||
showVersion = {
|
||||
withBGApi {
|
||||
val info = chatModel.controller.apiGetVersion()
|
||||
if (info != null) {
|
||||
ModalManager.start.showModal { VersionInfoView(info) }
|
||||
}
|
||||
}
|
||||
ModalManager.start.showModal(cardScreen = true) { VersionInfoView(showSettingsModal, ::doWithAuth) }
|
||||
},
|
||||
withAuth = ::doWithAuth,
|
||||
)
|
||||
@@ -84,8 +77,6 @@ fun SettingsLayout(
|
||||
stopped: Boolean,
|
||||
encrypted: Boolean,
|
||||
passphraseSaved: Boolean,
|
||||
notificationsMode: State<NotificationsMode>,
|
||||
userDisplayName: String?,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
@@ -98,30 +89,52 @@ fun SettingsLayout(
|
||||
LaunchedEffect(Unit) {
|
||||
hideKeyboard(view)
|
||||
}
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state }
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.your_settings))
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_settings)) {
|
||||
SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
|
||||
SectionView {
|
||||
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) })
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_chat_database)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.your_privacy), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.help_and_support), showSettingsModal { HelpAndSupportView(it, showModal, showCustomModal) })
|
||||
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView() }, stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped)
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.advanced_settings)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped)
|
||||
if (appPlatform == AppPlatform.ANDROID) {
|
||||
SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
|
||||
}
|
||||
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
|
||||
AppShutdownItem()
|
||||
AppVersionItem(showVersion)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HelpAndSupportView(
|
||||
chatModel: ChatModel,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
val userDisplayName = chatModel.currentUser.value?.displayName ?: ""
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.help_and_support))
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_help)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close = close) }, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) })
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_contact)) {
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
|
||||
}
|
||||
@@ -129,27 +142,29 @@ fun SettingsLayout(
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_support)) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_support_project)) {
|
||||
if (!BuildConfigCommon.ANDROID_BUNDLE) {
|
||||
ContributeItem(uriHandler)
|
||||
}
|
||||
RateAppItem(uriHandler)
|
||||
if (appPlatform.isAndroid) {
|
||||
RateAppItem(uriHandler)
|
||||
}
|
||||
StarOnGithubItem(uriHandler)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SettingsSectionApp(showSettingsModal, showVersion, withAuth)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun SettingsSectionApp(
|
||||
expect fun AdvancedSettingsAppSection(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
|
||||
)
|
||||
|
||||
// Shutdown is only available on Android; on desktop the app is closed via the window.
|
||||
@Composable
|
||||
expect fun AppShutdownItem()
|
||||
|
||||
@Composable private fun DatabaseItem(encrypted: Boolean, saved: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) {
|
||||
SectionItemView(openDatabaseView) {
|
||||
Row(
|
||||
@@ -160,11 +175,11 @@ expect fun SettingsSectionApp(
|
||||
Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_database),
|
||||
contentDescription = stringResource(MR.strings.database_passphrase_and_export),
|
||||
contentDescription = stringResource(MR.strings.chat_data),
|
||||
tint = if (encrypted && (appPlatform.isAndroid || !saved)) MaterialTheme.colors.secondary else WarningOrange,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(stringResource(MR.strings.database_passphrase_and_export))
|
||||
Text(stringResource(MR.strings.chat_data))
|
||||
}
|
||||
if (stopped) {
|
||||
Icon(
|
||||
@@ -208,7 +223,7 @@ fun ChatLockItem(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun ContributeItem(uriHandler: UriHandler) {
|
||||
@Composable fun ContributeItem(uriHandler: UriHandler) {
|
||||
SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat#contribute") }) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_keyboard),
|
||||
@@ -220,7 +235,7 @@ fun ChatLockItem(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun RateAppItem(uriHandler: UriHandler) {
|
||||
@Composable fun RateAppItem(uriHandler: UriHandler) {
|
||||
SectionItemView({
|
||||
runCatching { uriHandler.openUriCatching("market://details?id=chat.simplex.app") }
|
||||
.onFailure { uriHandler.openUriCatching("https://play.google.com/store/apps/details?id=chat.simplex.app") }
|
||||
@@ -236,7 +251,7 @@ fun ChatLockItem(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun StarOnGithubItem(uriHandler: UriHandler) {
|
||||
@Composable fun StarOnGithubItem(uriHandler: UriHandler) {
|
||||
SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat") }) {
|
||||
Icon(
|
||||
painter = painterResource(MR.images.ic_github),
|
||||
@@ -486,8 +501,6 @@ fun PreviewSettingsLayout() {
|
||||
stopped = false,
|
||||
encrypted = false,
|
||||
passphraseSaved = false,
|
||||
notificationsMode = remember { mutableStateOf(NotificationsMode.OFF) },
|
||||
userDisplayName = "Alice",
|
||||
setPerformLA = { _ -> },
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
|
||||
+38
-16
@@ -1,33 +1,55 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionView
|
||||
import itemHPadding
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.CoreVersionInfo
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
||||
import chat.simplex.common.views.helpers.AppBarTitle
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun VersionInfoView(info: CoreVersionInfo) {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false)
|
||||
if (appPlatform.isAndroid) {
|
||||
Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME))
|
||||
Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE))
|
||||
} else {
|
||||
Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.DESKTOP_VERSION_NAME))
|
||||
Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.DESKTOP_VERSION_CODE))
|
||||
fun VersionInfoView(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
|
||||
) {
|
||||
val versionInfo = remember { mutableStateOf<CoreVersionInfo?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
versionInfo.value = chatModel.controller.apiGetVersion()
|
||||
}
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.app_version_title))
|
||||
SectionView {
|
||||
Column(Modifier.padding(horizontal = itemHPadding, vertical = DEFAULT_PADDING_HALF)) {
|
||||
if (appPlatform.isAndroid) {
|
||||
Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME))
|
||||
Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE))
|
||||
} else {
|
||||
Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.DESKTOP_VERSION_NAME))
|
||||
Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.DESKTOP_VERSION_CODE))
|
||||
}
|
||||
versionInfo.value?.let { info ->
|
||||
Text(String.format(stringResource(MR.strings.core_version), info.version))
|
||||
val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
|
||||
Text(String.format(stringResource(MR.strings.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(String.format(stringResource(MR.strings.core_version), info.version))
|
||||
val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
|
||||
Text(String.format(stringResource(MR.strings.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
|
||||
SectionDividerSpaced()
|
||||
|
||||
AdvancedSettingsAppSection(showSettingsModal, withAuth)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1555,6 +1555,13 @@
|
||||
<string name="settings_section_title_files">Files</string>
|
||||
<string name="settings_section_title_delivery_receipts">Send delivery receipts to</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">Contact requests from groups</string>
|
||||
<string name="settings_section_title_about">About</string>
|
||||
<string name="settings_section_title_contact">Contact</string>
|
||||
<string name="settings_section_title_support_project">Support the project</string>
|
||||
<string name="chat_data">Chat data</string>
|
||||
<string name="help_and_support">Help & support</string>
|
||||
<string name="more_privacy">More privacy</string>
|
||||
<string name="advanced_settings">Advanced settings</string>
|
||||
<string name="settings_restart_app">Restart</string>
|
||||
<string name="settings_shutdown">Shutdown</string>
|
||||
<string name="settings_developer_tools">Developer tools</string>
|
||||
@@ -2681,7 +2688,7 @@
|
||||
<string name="dont_enable_receipts">Don\'t enable</string>
|
||||
<string name="you_can_enable_delivery_receipts_later">You can enable later via Settings</string>
|
||||
<string name="delivery_receipts_are_disabled">Delivery receipts are disabled!</string>
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Privacy & Security settings.</string>
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Your privacy settings.</string>
|
||||
<string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string>
|
||||
|
||||
<!-- Remote access -->
|
||||
|
||||
+8
-12
@@ -1,27 +1,21 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.runtime.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.AppUpdatesChannel
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
actual fun SettingsSectionApp(
|
||||
actual fun AdvancedSettingsAppSection(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_app)) {
|
||||
SectionView {
|
||||
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) })
|
||||
val selectedChannel = remember { appPrefs.appUpdateChannel.state }
|
||||
val values = AppUpdatesChannel.entries.map { it to it.text }
|
||||
@@ -29,6 +23,8 @@ actual fun SettingsSectionApp(
|
||||
appPrefs.appUpdateChannel.set(it)
|
||||
setupUpdateChecker()
|
||||
}
|
||||
AppVersionItem(showVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun AppShutdownItem() {}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user