From 1093892edee889a6783c8aaead7725f61302b1da Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 29 Mar 2023 08:41:13 +0100
Subject: [PATCH 1/5] ios: update developer options (#2091)
* ios: update developer options
* move XFTP option, make chat console usable
* update footer
* typo
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
---------
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
---
apps/ios/Shared/Model/ChatModel.swift | 2 +-
apps/ios/Shared/Views/TerminalView.swift | 37 ++++++++++------
.../Views/UserSettings/CallSettings.swift | 1 -
.../Views/UserSettings/DeveloperView.swift | 44 ++++++++++++++-----
.../Views/UserSettings/PrivacySettings.swift | 1 -
.../Views/UserSettings/SettingsView.swift | 12 ++---
6 files changed, 64 insertions(+), 33 deletions(-)
diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift
index e8f917af3f..6f33e42640 100644
--- a/apps/ios/Shared/Model/ChatModel.swift
+++ b/apps/ios/Shared/Model/ChatModel.swift
@@ -381,7 +381,7 @@ final class ChatModel: ObservableObject {
markChatItemsRead(cInfo)
}
}
-
+
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
_updateChat(cInfo.id) { chat in
chat.chatStats.unreadChat = unreadChat
diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift
index 4174e5b439..763627fc47 100644
--- a/apps/ios/Shared/Views/TerminalView.swift
+++ b/apps/ios/Shared/Views/TerminalView.swift
@@ -20,6 +20,7 @@ struct TerminalView: View {
@State var composeState: ComposeState = ComposeState()
@FocusState private var keyboardVisible: Bool
@State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
+ @State private var terminalItem: TerminalItem?
var body: some View {
if authorized {
@@ -38,19 +39,8 @@ struct TerminalView: View {
ScrollView {
LazyVStack {
ForEach(chatModel.terminalItems) { item in
- NavigationLink {
- let s = item.details
- ScrollView {
- Text(s.prefix(maxItemSize))
- .padding()
- }
- .toolbar {
- ToolbarItem(placement: .navigationBarTrailing) {
- Button { showShareSheet(items: [s]) } label: {
- Image(systemName: "square.and.arrow.up")
- }
- }
- }
+ Button {
+ terminalItem = item
} label: {
HStack {
Text(item.id.formatted(date: .omitted, time: .standard))
@@ -70,6 +60,11 @@ struct TerminalView: View {
}
}
}
+ .background(NavigationLink(
+ isActive: Binding(get: { terminalItem != nil }, set: { _ in }),
+ destination: terminalItemView,
+ label: { EmptyView() }
+ ))
}
}
@@ -96,6 +91,22 @@ struct TerminalView: View {
}
}
}
+
+ func terminalItemView() -> some View {
+ let s = terminalItem?.details ?? ""
+ return ScrollView {
+ Text(s.prefix(maxItemSize))
+ .padding()
+ }
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button { showShareSheet(items: [s]) } label: {
+ Image(systemName: "square.and.arrow.up")
+ }
+ }
+ }
+ .onDisappear { terminalItem = nil }
+ }
func sendMessage() {
let cmd = ChatCommand.string(composeState.message)
diff --git a/apps/ios/Shared/Views/UserSettings/CallSettings.swift b/apps/ios/Shared/Views/UserSettings/CallSettings.swift
index 43c715523e..3409e7ab0e 100644
--- a/apps/ios/Shared/Views/UserSettings/CallSettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/CallSettings.swift
@@ -13,7 +13,6 @@ struct CallSettings: View {
@AppStorage(DEFAULT_WEBRTC_POLICY_RELAY) private var webrtcPolicyRelay = true
@AppStorage(GROUP_DEFAULT_CALL_KIT_ENABLED, store: groupDefaults) private var callKitEnabled = true
@AppStorage(DEFAULT_CALL_KIT_CALLS_IN_RECENTS) private var callKitCallsInRecents = false
- @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
private let allowChangingCallsHistory = false
var body: some View {
diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
index 0d7435d909..9f3a6684dd 100644
--- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
+++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
@@ -12,23 +12,13 @@ import SimpleXChat
struct DeveloperView: View {
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false
+ @AppStorage(GROUP_DEFAULT_XFTP_SEND_ENABLED, store: groupDefaults) private var xftpSendEnabled = false
@Environment(\.colorScheme) var colorScheme
var body: some View {
VStack {
List {
Section {
- NavigationLink {
- TerminalView()
- } label: {
- settingsRow("terminal") { Text("Chat console") }
- }
- settingsRow("chevron.left.forwardslash.chevron.right") {
- Toggle("Show developer options", isOn: $developerTools)
- }
- settingsRow("internaldrive") {
- Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades)
- }
ZStack(alignment: .leading) {
Image(colorScheme == .dark ? "github_light" : "github")
.resizable()
@@ -37,6 +27,38 @@ struct DeveloperView: View {
Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)")
.padding(.leading, 36)
}
+ NavigationLink {
+ TerminalView()
+ } label: {
+ settingsRow("terminal") { Text("Chat console") }
+ }
+ settingsRow("internaldrive") {
+ Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades)
+ }
+ settingsRow("chevron.left.forwardslash.chevron.right") {
+ Toggle("Show developer options", isOn: $developerTools)
+ }
+ } footer: {
+ (developerTools ? Text("Show: ") : Text("Hide: ")) + Text("Database IDs and Transport isolation option.")
+ }
+
+ Section {
+ settingsRow("arrow.up.doc") {
+ Toggle("Send files via XFTP", isOn: $xftpSendEnabled)
+ .onChange(of: xftpSendEnabled) { _ in
+ do {
+ try setXFTPConfig(getXFTPCfg())
+ } catch {
+ logger.error("setXFTPConfig: cannot set XFTP config \(responseError(error))")
+ }
+ }
+ }
+ } header: {
+ Text("Experimental")
+ } footer: {
+ if xftpSendEnabled {
+ Text("v4.6.1+ is required to receive via XFTP.")
+ }
}
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
index 5b9cd49526..4c0084b68b 100644
--- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
@@ -12,7 +12,6 @@ import SimpleXChat
struct PrivacySettings: View {
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
- @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
index 715533481a..e885fc1f2d 100644
--- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
@@ -264,12 +264,12 @@ struct SettingsView: View {
} label: {
settingsRow("chevron.left.forwardslash.chevron.right") { Text("Developer tools") }
}
- NavigationLink {
- ExperimentalFeaturesView()
- .navigationTitle("Experimental features")
- } label: {
- settingsRow("gauge") { Text("Experimental features") }
- }
+// NavigationLink {
+// ExperimentalFeaturesView()
+// .navigationTitle("Experimental features")
+// } label: {
+// settingsRow("gauge") { Text("Experimental features") }
+// }
NavigationLink {
VersionView()
.navigationBarTitle("App version")
From 67961180c917c4cf4bd7038d4782844aafdd6824 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 29 Mar 2023 09:34:55 +0100
Subject: [PATCH 2/5] android: developer tools page (#2094)
---
.../app/views/usersettings/DeveloperView.kt | 34 ++++++++++++++-----
.../app/views/usersettings/SettingsView.kt | 6 ++--
.../app/src/main/res/values/strings.xml | 6 ++++
3 files changed, 34 insertions(+), 12 deletions(-)
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt
index e0fd6d276f..f20cde508a 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt
@@ -1,6 +1,8 @@
package chat.simplex.app.views.usersettings
import SectionDivider
+import SectionSpacer
+import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -14,7 +16,7 @@ import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.TerminalView
-import chat.simplex.app.views.helpers.AppBarTitle
+import chat.simplex.app.views.helpers.*
@Composable
fun DeveloperView(
@@ -23,20 +25,34 @@ fun DeveloperView(
withAuth: (block: () -> Unit) -> Unit
) {
Column(Modifier.fillMaxWidth()) {
- val developerTools = m.controller.appPrefs.developerTools
- val confirmDBUpgrades = m.controller.appPrefs.confirmDBUpgrades
val uriHandler = LocalUriHandler.current
AppBarTitle(stringResource(R.string.settings_developer_tools))
+ val developerTools = m.controller.appPrefs.developerTools
+ val devTools = remember { mutableStateOf(developerTools.get()) }
SectionView() {
+ InstallTerminalAppItem(uriHandler)
+ SectionDivider()
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
SectionDivider()
- val devTools = remember { mutableStateOf(developerTools.get()) }
- SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
+ SettingsPreferenceItem(Icons.Outlined.DriveFolderUpload, stringResource(R.string.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
SectionDivider()
- var confirm = remember { mutableStateOf(confirmDBUpgrades.get()) }
- SettingsPreferenceItem(Icons.Outlined.DriveFolderUpload, stringResource(R.string.confirm_database_upgrades), confirmDBUpgrades, confirm)
- SectionDivider()
- InstallTerminalAppItem(uriHandler)
+ SettingsPreferenceItem(Icons.Outlined.Code, stringResource(R.string.show_developer_options), developerTools, devTools)
+ }
+ SectionTextFooter(
+ generalGetString(if (devTools.value) R.string.show_dev_options else R.string.hide_dev_options) +
+ generalGetString(R.string.developer_options)
+ )
+ SectionSpacer()
+
+ val xftpSendEnabled = m.controller.appPrefs.xftpSendEnabled
+ val xftpEnabled = remember { mutableStateOf(xftpSendEnabled.get()) }
+ SectionView(generalGetString(R.string.settings_section_title_experimenta)) {
+ SettingsPreferenceItem(Icons.Outlined.UploadFile, stringResource(R.string.settings_send_files_via_xftp), xftpSendEnabled, xftpEnabled) {
+ withApi { m.controller.apiSetXFTPConfig(m.controller.getXFTPCfg()) }
+ }
+ }
+ if (xftpEnabled.value) {
+ SectionTextFooter(generalGetString(R.string.xftp_requires_v461))
}
}
}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt
index f55eb941ba..e07b3443d0 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt
@@ -205,10 +205,10 @@ fun SettingsLayout(
SectionSpacer()
SectionView(stringResource(R.string.settings_section_title_develop)) {
- SettingsActionItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
- SectionDivider()
- SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it) })
+ SettingsActionItem(Icons.Outlined.Code, stringResource(R.string.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
SectionDivider()
+// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it) })
+// SectionDivider()
AppVersionItem(showVersion)
}
}
diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml
index 2f8206c58a..045da6c4b4 100644
--- a/apps/android/app/src/main/res/values/strings.xml
+++ b/apps/android/app/src/main/res/values/strings.xml
@@ -509,6 +509,10 @@
Core version: v%s
Core built at: %s
simplexmq: v%s (%2s)
+ Show:\
+ Hide:\
+ Show developer options
+ Database IDs and Transport isolation option.
Create address
@@ -716,7 +720,9 @@
MESSAGES
CALLS
Incognito mode
+ EXPERIMENTAL
Send files via XFTP
+ v4.6.1+ is required to receive via XFTP.
Your chat database
From 08dd321311d6a2bf15ee3a9ad7dd714af27c7693 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 29 Mar 2023 15:48:00 +0400
Subject: [PATCH 3/5] android: rcv & snd files progress, distinguish XFTP and
SMP; ios: files UI improvements (#2096)
---
.../java/chat/simplex/app/model/ChatModel.kt | 15 +++--
.../java/chat/simplex/app/model/SimpleXAPI.kt | 10 ++++
.../simplex/app/views/chat/ComposeView.kt | 10 ++--
.../simplex/app/views/chat/item/CIFileView.kt | 55 ++++++++++++++++---
.../app/views/chat/item/CIImageView.kt | 49 +++++++++++------
.../app/views/chat/item/CIVoiceView.kt | 2 +-
.../chat/simplex/app/views/helpers/Util.kt | 16 ++++--
.../app/src/main/res/values/strings.xml | 2 +
.../Views/Chat/ChatItem/CIFileView.swift | 18 ++++--
.../Views/Chat/ChatItem/CIImageView.swift | 13 ++++-
apps/ios/SimpleXChat/ChatTypes.swift | 7 ---
11 files changed, 145 insertions(+), 52 deletions(-)
diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
index e3351dc124..7612ec8304 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
@@ -1711,7 +1711,8 @@ class CIFile(
val fileName: String,
val fileSize: Long,
val filePath: String? = null,
- val fileStatus: CIFileStatus
+ val fileStatus: CIFileStatus,
+ val fileProtocol: FileProtocol
) {
val loaded: Boolean = when (fileStatus) {
is CIFileStatus.SndStored -> true
@@ -1733,19 +1734,25 @@ class CIFile(
filePath: String? = "test.txt",
fileStatus: CIFileStatus = CIFileStatus.RcvComplete
): CIFile =
- CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus)
+ CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus, fileProtocol = FileProtocol.XFTP)
}
}
+@Serializable
+enum class FileProtocol {
+ @SerialName("smp") SMP,
+ @SerialName("xftp") XFTP;
+}
+
@Serializable
sealed class CIFileStatus {
@Serializable @SerialName("sndStored") object SndStored: CIFileStatus()
- @Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Int, val sndTotal: Int): CIFileStatus()
+ @Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus()
@Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
@Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
- @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Int, val rcvTotal: Int): CIFileStatus()
+ @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
index 04f721ec3a..c8ceacae5a 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
@@ -1402,6 +1402,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileComplete ->
chatItemSimpleUpdate(r.user, r.chatItem)
+ is CR.RcvFileProgressXFTP ->
+ chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileStart ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileComplete -> {
@@ -1417,6 +1419,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
removeFile(appContext, fileName)
}
}
+ is CR.SndFileProgressXFTP ->
+ chatItemSimpleUpdate(r.user, r.chatItem)
is CR.CallInvitation -> {
chatModel.callManager.reportNewIncomingCall(r.callInvitation)
}
@@ -3044,12 +3048,14 @@ sealed class CR {
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: User, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: User, val chatItem: AChatItem): CR()
@Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: User, val chatItem: AChatItem): CR()
+ @Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: User, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): CR()
// sending file events
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndGroupFileCancelled") class SndGroupFileCancelled(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List): CR()
+ @Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR()
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
@Serializable @SerialName("callOffer") class CallOffer(val user: User, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR()
@Serializable @SerialName("callAnswer") class CallAnswer(val user: User, val contact: Contact, val answer: WebRTCSession): CR()
@@ -3146,11 +3152,13 @@ sealed class CR {
is RcvFileAccepted -> "rcvFileAccepted"
is RcvFileStart -> "rcvFileStart"
is RcvFileComplete -> "rcvFileComplete"
+ is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
is SndFileCancelled -> "sndFileCancelled"
is SndFileComplete -> "sndFileComplete"
is SndFileRcvCancelled -> "sndFileRcvCancelled"
is SndFileStart -> "sndFileStart"
is SndGroupFileCancelled -> "sndGroupFileCancelled"
+ is SndFileProgressXFTP -> "sndFileProgressXFTP"
is CallInvitation -> "callInvitation"
is CallOffer -> "callOffer"
is CallAnswer -> "callAnswer"
@@ -3249,11 +3257,13 @@ sealed class CR {
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
is RcvFileStart -> withUser(user, json.encodeToString(chatItem))
is RcvFileComplete -> withUser(user, json.encodeToString(chatItem))
+ is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
is SndFileCancelled -> json.encodeToString(chatItem)
is SndFileComplete -> withUser(user, json.encodeToString(chatItem))
is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem))
is SndFileStart -> withUser(user, json.encodeToString(chatItem))
is SndGroupFileCancelled -> withUser(user, json.encodeToString(chatItem))
+ is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize")
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt
index d63385ee2e..574e56f740 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt
@@ -182,6 +182,8 @@ fun ComposeView(
val pendingLinkUrl = rememberSaveable { mutableStateOf(null) }
val cancelledLinks = rememberSaveable { mutableSetOf() }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
+ val xftpSendEnabled = chatModel.controller.appPrefs.xftpSendEnabled.get()
+ val maxFileSize = getMaxFileSize(fileProtocol = if (xftpSendEnabled) FileProtocol.XFTP else FileProtocol.SMP)
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
@@ -212,13 +214,13 @@ fun ComposeView(
if (isAnimNewApi || isAnimOldApi) {
// It's a gif or webp
val fileSize = getFileSize(context, uri)
- if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
+ if (fileSize != null && fileSize <= maxFileSize) {
content.add(UploadContent.AnimatedImage(uri))
} else {
bitmap = null
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
- String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(MAX_FILE_SIZE))
+ String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
)
}
} else {
@@ -236,7 +238,7 @@ fun ComposeView(
val processPickedFile = { uri: Uri?, text: String? ->
if (uri != null) {
val fileSize = getFileSize(context, uri)
- if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
+ if (fileSize != null && fileSize <= maxFileSize) {
val fileName = getFileName(SimplexApp.context, uri)
if (fileName != null) {
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
@@ -244,7 +246,7 @@ fun ComposeView(
} else {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
- String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(MAX_FILE_SIZE))
+ String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
)
}
}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt
index 0df2c1518e..ac759cd2ce 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt
@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat.item
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@@ -16,6 +17,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.dp
@@ -64,7 +66,7 @@ fun CIFileView(
fun fileSizeValid(): Boolean {
if (file != null) {
- return file.fileSize <= MAX_FILE_SIZE
+ return file.fileSize <= getMaxFileSize(file.fileProtocol)
}
return false
}
@@ -78,15 +80,23 @@ fun CIFileView(
} else {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
- String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(MAX_FILE_SIZE))
+ String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
)
}
}
is CIFileStatus.RcvAccepted ->
- AlertManager.shared.showAlertMsg(
- generalGetString(R.string.waiting_for_file),
- String.format(generalGetString(R.string.file_will_be_received_when_contact_is_online), MAX_FILE_SIZE)
- )
+ when (file.fileProtocol) {
+ FileProtocol.XFTP ->
+ AlertManager.shared.showAlertMsg(
+ generalGetString(R.string.waiting_for_file),
+ generalGetString(R.string.file_will_be_received_when_contact_completes_uploading)
+ )
+ FileProtocol.SMP ->
+ AlertManager.shared.showAlertMsg(
+ generalGetString(R.string.waiting_for_file),
+ generalGetString(R.string.file_will_be_received_when_contact_is_online)
+ )
+ }
is CIFileStatus.RcvComplete -> {
val filePath = getLoadedFilePath(context, file)
if (filePath != null) {
@@ -109,6 +119,20 @@ fun CIFileView(
)
}
+ @Composable
+ fun progressCircle(progress: Long, total: Long) {
+ val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat()
+ val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
+ val strokeColor = if (isInDarkTheme()) FileDark else FileLight
+ Surface(
+ Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
+ color = Color.Transparent,
+ shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
+ ) {
+ Box(Modifier.size(32.dp))
+ }
+ }
+
@Composable
fun fileIndicator() {
Box(
@@ -120,8 +144,16 @@ fun CIFileView(
) {
if (file != null) {
when (file.fileStatus) {
- is CIFileStatus.SndStored -> fileIcon()
- is CIFileStatus.SndTransfer -> progressIndicator()
+ is CIFileStatus.SndStored ->
+ when (file.fileProtocol) {
+ FileProtocol.XFTP -> progressIndicator()
+ FileProtocol.SMP -> fileIcon()
+ }
+ is CIFileStatus.SndTransfer ->
+ when (file.fileProtocol) {
+ FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
+ FileProtocol.SMP -> progressIndicator()
+ }
is CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check)
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.RcvInvitation ->
@@ -130,7 +162,12 @@ fun CIFileView(
else
fileIcon(innerIcon = Icons.Outlined.PriorityHigh, color = WarningOrange)
is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = Icons.Outlined.MoreHoriz)
- is CIFileStatus.RcvTransfer -> progressIndicator()
+ is CIFileStatus.RcvTransfer ->
+ if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
+ progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
+ } else {
+ progressIndicator()
+ }
is CIFileStatus.RcvComplete -> fileIcon()
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt
index 7fb2e92f93..8599dc9137 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt
@@ -27,8 +27,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.R
-import chat.simplex.app.model.CIFile
-import chat.simplex.app.model.CIFileStatus
+import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.*
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
@@ -45,6 +44,15 @@ fun CIImageView(
showMenu: MutableState,
receiveFile: (Long) -> Unit
) {
+ @Composable
+ fun progressIndicator() {
+ CircularProgressIndicator(
+ Modifier.size(16.dp),
+ color = Color.White,
+ strokeWidth = 2.dp
+ )
+ }
+
@Composable
fun loadingIndicator() {
if (file != null) {
@@ -55,12 +63,13 @@ fun CIImageView(
contentAlignment = Alignment.Center
) {
when (file.fileStatus) {
+ is CIFileStatus.SndStored ->
+ when (file.fileProtocol) {
+ FileProtocol.XFTP -> progressIndicator()
+ FileProtocol.SMP -> {}
+ }
is CIFileStatus.SndTransfer ->
- CircularProgressIndicator(
- Modifier.size(16.dp),
- color = Color.White,
- strokeWidth = 2.dp
- )
+ progressIndicator()
is CIFileStatus.SndComplete ->
Icon(
Icons.Filled.Check,
@@ -76,11 +85,7 @@ fun CIImageView(
tint = Color.White
)
is CIFileStatus.RcvTransfer ->
- CircularProgressIndicator(
- Modifier.size(16.dp),
- color = Color.White,
- strokeWidth = 2.dp
- )
+ progressIndicator()
is CIFileStatus.RcvInvitation ->
Icon(
Icons.Outlined.ArrowDownward,
@@ -136,7 +141,7 @@ fun CIImageView(
fun fileSizeValid(): Boolean {
if (file != null) {
- return file.fileSize <= MAX_FILE_SIZE
+ return file.fileSize <= getMaxFileSize(file.fileProtocol)
}
return false
}
@@ -179,14 +184,22 @@ fun CIImageView(
} else {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
- String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(MAX_FILE_SIZE))
+ String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
)
}
CIFileStatus.RcvAccepted ->
- AlertManager.shared.showAlertMsg(
- generalGetString(R.string.waiting_for_image),
- generalGetString(R.string.image_will_be_received_when_contact_is_online)
- )
+ when (file.fileProtocol) {
+ FileProtocol.XFTP ->
+ AlertManager.shared.showAlertMsg(
+ generalGetString(R.string.waiting_for_image),
+ generalGetString(R.string.image_will_be_received_when_contact_completes_uploading)
+ )
+ FileProtocol.SMP ->
+ AlertManager.shared.showAlertMsg(
+ generalGetString(R.string.waiting_for_image),
+ generalGetString(R.string.image_will_be_received_when_contact_is_online)
+ )
+ }
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
CIFileStatus.RcvComplete -> {} // ?
CIFileStatus.RcvCancelled -> {} // TODO
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt
index c20df776a2..cc873ac6ec 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt
@@ -228,7 +228,7 @@ private fun VoiceMsgIndicator(
}
}
-private fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = drawWithCache {
+fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = drawWithCache {
val brush = Brush.linearGradient(
0f to Color.Transparent,
0f to color,
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt
index cdb5675545..d361daa9af 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt
@@ -237,8 +237,9 @@ const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE
const val MAX_VOICE_SIZE_FOR_SENDING: Long = 94680 // 6 chunks * 15780 bytes per chunk
const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 43_000
-//const val MAX_FILE_SIZE_SMP: Long = 8000000 // TODO distinguish between XFTP and SMP files
-const val MAX_FILE_SIZE: Long = 1_073_741_824
+const val MAX_FILE_SIZE_SMP: Long = 8000000
+
+const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824
fun getFilesDirectory(context: Context): String {
return context.filesDir.toString()
@@ -494,9 +495,9 @@ fun formatBytes(bytes: Long): String {
return "0 bytes"
}
val bytesDouble = bytes.toDouble()
- val k = 1000.toDouble()
+ val k = 1024.toDouble()
val units = arrayOf("bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
- val i = kotlin.math.floor(log2(bytesDouble) / log2(k))
+ val i = floor(log2(bytesDouble) / log2(k))
val size = bytesDouble / k.pow(i)
val unit = units[i.toInt()]
@@ -541,6 +542,13 @@ fun directoryFileCountAndSize(dir: String): Pair { // count, size in
return fileCount to bytes
}
+fun getMaxFileSize(fileProtocol: FileProtocol): Long {
+ return when (fileProtocol) {
+ FileProtocol.XFTP -> MAX_FILE_SIZE_XFTP
+ FileProtocol.SMP -> MAX_FILE_SIZE_SMP
+ }
+}
+
fun Color.darker(factor: Float = 0.1f): Color =
Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha)
diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml
index 045da6c4b4..aa6d9f453e 100644
--- a/apps/android/app/src/main/res/values/strings.xml
+++ b/apps/android/app/src/main/res/values/strings.xml
@@ -238,6 +238,7 @@
Asked to receive the image
Image sent
Waiting for image
+ Image will be received when your contact completes uploading it.
Image will be received when your contact is online, please wait or check later!
Image saved to Gallery
@@ -247,6 +248,7 @@
Your contact sent a file that is larger than currently supported maximum size (%1$s).
Currently maximum supported file size is %1$s.
Waiting for file
+ File will be received when your contact completes uploading it.
File will be received when your contact is online, please wait or check later!
File saved
File not found
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
index 170f2d7cd5..8e6e4af4f1 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
@@ -56,7 +56,7 @@ struct CIFileView: View {
case .sndComplete: return false
case .sndCancelled: return false
case .rcvInvitation: return true
- case .rcvAccepted: return file.isSMP
+ case .rcvAccepted: return true
case .rcvTransfer: return false
case .rcvComplete: return true
case .rcvCancelled: return false
@@ -92,7 +92,13 @@ struct CIFileView: View {
)
}
case .rcvAccepted:
- if file.isSMP {
+ switch file.fileProtocol {
+ case .xftp:
+ AlertManager.shared.showAlertMsg(
+ title: "Waiting for file",
+ message: "File will be received when your contact completes uploading it."
+ )
+ case .smp:
AlertManager.shared.showAlertMsg(
title: "Waiting for file",
message: "File will be received when your contact is online, please wait or check later!"
@@ -112,7 +118,11 @@ struct CIFileView: View {
@ViewBuilder private func fileIndicator() -> some View {
if let file = file {
switch file.fileStatus {
- case .sndStored: fileIcon("doc.fill")
+ case .sndStored:
+ switch file.fileProtocol {
+ case .xftp: progressView()
+ case .smp: fileIcon("doc.fill")
+ }
case let .sndTransfer(sndProgress, sndTotal):
switch file.fileProtocol {
case .xftp: progressCircle(sndProgress, sndTotal)
@@ -169,7 +179,7 @@ struct CIFileView: View {
Circle()
.trim(from: 0, to: Double(progress) / Double(total))
.stroke(
- Color.accentColor,
+ Color(uiColor: .tertiaryLabel),
style: StrokeStyle(lineWidth: 3)
)
.rotationEffect(.degrees(-90))
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
index a52cd72a22..d53639cd29 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
@@ -41,7 +41,13 @@ struct CIImageView: View {
// TODO image accepted alert?
}
case .rcvAccepted:
- if file.isSMP {
+ switch file.fileProtocol {
+ case .xftp:
+ AlertManager.shared.showAlertMsg(
+ title: "Waiting for image",
+ message: "Image will be received when your contact completes uploading it."
+ )
+ case .smp:
AlertManager.shared.showAlertMsg(
title: "Waiting for image",
message: "Image will be received when your contact is online, please wait or check later!"
@@ -79,6 +85,11 @@ struct CIImageView: View {
@ViewBuilder private func loadingIndicator() -> some View {
if let file = chatItem.file {
switch file.fileStatus {
+ case .sndStored:
+ switch file.fileProtocol {
+ case .xftp: progressView()
+ case .smp: EmptyView()
+ }
case .sndTransfer:
progressView()
case .sndComplete:
diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift
index f1e9ba0a4e..3b3b4acd22 100644
--- a/apps/ios/SimpleXChat/ChatTypes.swift
+++ b/apps/ios/SimpleXChat/ChatTypes.swift
@@ -2221,13 +2221,6 @@ public struct CIFile: Decodable {
CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus, fileProtocol: .xftp)
}
- public var isSMP: Bool {
- switch self.fileProtocol {
- case .smp: return true
- default: return false
- }
- }
-
public var loaded: Bool {
get {
switch self.fileStatus {
From ade7bba97bdd7d5c99995e698f2bd84342c50ec2 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 29 Mar 2023 14:01:24 +0100
Subject: [PATCH 4/5] ios: confirm password when deleting active hidden user
(#2095)
---
.../app/views/usersettings/DeveloperView.kt | 2 +-
.../app/src/main/res/values/strings.xml | 4 +-
.../Views/UserSettings/DeveloperView.swift | 2 +-
.../UserSettings/HiddenProfileView.swift | 6 +-
.../Views/UserSettings/UserProfilesView.swift | 98 +++++++++++++++----
5 files changed, 87 insertions(+), 25 deletions(-)
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt
index f20cde508a..55a6dee8ab 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt
@@ -39,7 +39,7 @@ fun DeveloperView(
SettingsPreferenceItem(Icons.Outlined.Code, stringResource(R.string.show_developer_options), developerTools, devTools)
}
SectionTextFooter(
- generalGetString(if (devTools.value) R.string.show_dev_options else R.string.hide_dev_options) +
+ generalGetString(if (devTools.value) R.string.show_dev_options else R.string.hide_dev_options) + " " +
generalGetString(R.string.developer_options)
)
SectionSpacer()
diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml
index aa6d9f453e..8137e48ab4 100644
--- a/apps/android/app/src/main/res/values/strings.xml
+++ b/apps/android/app/src/main/res/values/strings.xml
@@ -511,8 +511,8 @@
Core version: v%s
Core built at: %s
simplexmq: v%s (%2s)
- Show:\
- Hide:\
+ Show:
+ Hide:
Show developer options
Database IDs and Transport isolation option.
diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
index 9f3a6684dd..ce0fd5caeb 100644
--- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
+++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
@@ -39,7 +39,7 @@ struct DeveloperView: View {
Toggle("Show developer options", isOn: $developerTools)
}
} footer: {
- (developerTools ? Text("Show: ") : Text("Hide: ")) + Text("Database IDs and Transport isolation option.")
+ (developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
}
Section {
diff --git a/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift b/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift
index d01fee92fa..509874619f 100644
--- a/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift
+++ b/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift
@@ -33,7 +33,7 @@ struct HiddenProfileView: View {
}
Section {
- PassphraseField(key: $hidePassword, placeholder: "Password to show", valid: true, showStrength: true)
+ PassphraseField(key: $hidePassword, placeholder: "Password to show", valid: passwordValid, showStrength: true)
PassphraseField(key: $confirmHidePassword, placeholder: "Confirm password", valid: confirmValid)
settingsRow("lock") {
@@ -72,9 +72,11 @@ struct HiddenProfileView: View {
}
}
+ var passwordValid: Bool { hidePassword == hidePassword.trimmingCharacters(in: .whitespaces) }
+
var confirmValid: Bool { confirmHidePassword == "" || hidePassword == confirmHidePassword }
- var saveDisabled: Bool { hidePassword == "" || confirmHidePassword == "" || !confirmValid }
+ var saveDisabled: Bool { hidePassword == "" || !passwordValid || confirmHidePassword == "" || !confirmValid }
}
struct ProfilePrivacyView_Previews: PreviewProvider {
diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
index 01de1a8b33..ef41b9919e 100644
--- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
@@ -13,15 +13,17 @@ struct UserProfilesView: View {
@AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true
@AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true
@State private var showDeleteConfirmation = false
- @State private var userToDelete: UserInfo?
+ @State private var userToDelete: User?
@State private var alert: UserProfilesAlert?
@State private var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
@State private var searchTextOrPassword = ""
@State private var selectedUser: User?
@State private var profileHidden = false
+ @State private var profileAction: UserProfileAction?
+ @State private var actionPassword = ""
private enum UserProfilesAlert: Identifiable {
- case deleteUser(userInfo: UserInfo, delSMPQueues: Bool)
+ case deleteUser(user: User, delSMPQueues: Bool)
case cantDeleteLastUser
case hiddenProfilesNotice
case muteProfileAlert
@@ -30,7 +32,7 @@ struct UserProfilesView: View {
var id: String {
switch self {
- case let .deleteUser(userInfo, delSMPQueues): return "deleteUser \(userInfo.user.userId) \(delSMPQueues)"
+ case let .deleteUser(user, delSMPQueues): return "deleteUser \(user.userId) \(delSMPQueues)"
case .cantDeleteLastUser: return "cantDeleteLastUser"
case .hiddenProfilesNotice: return "hiddenProfilesNotice"
case .muteProfileAlert: return "muteProfileAlert"
@@ -40,6 +42,16 @@ struct UserProfilesView: View {
}
}
+ private enum UserProfileAction: Identifiable {
+ case deleteUser(user: User, delSMPQueues: Bool)
+
+ var id: String {
+ switch self {
+ case let .deleteUser(user, delSMPQueues): return "deleteUser \(user.userId) \(delSMPQueues)"
+ }
+ }
+ }
+
var body: some View {
if authorized {
userProfilesView()
@@ -69,7 +81,7 @@ struct UserProfilesView: View {
if let i = indexSet.first {
if m.users.count > 1 && (m.users[i].user.hidden || visibleUsersCount > 1) {
showDeleteConfirmation = true
- userToDelete = users[i]
+ userToDelete = users[i].user
} else {
alert = .cantDeleteLastUser
}
@@ -114,14 +126,17 @@ struct UserProfilesView: View {
withAnimation { profileHidden = false }
}
}
+ .sheet(item: $profileAction) { action in
+ profileActionView(action)
+ }
.alert(item: $alert) { alert in
switch alert {
- case let .deleteUser(userInfo, delSMPQueues):
+ case let .deleteUser(user, delSMPQueues):
return Alert(
title: Text("Delete user profile?"),
message: Text("All chats and messages will be deleted - this cannot be undone!"),
primaryButton: .destructive(Text("Delete")) {
- Task { await removeUser(userInfo, delSMPQueues) }
+ Task { await removeUser(user, delSMPQueues, viewPwd: userViewPassword(user)) }
},
secondaryButton: .cancel()
)
@@ -179,36 +194,81 @@ struct UserProfilesView: View {
m.users.filter({ u in !u.user.hidden }).count
}
- private func userViewPassword(_ user: User) -> String? {
- user.activeUser || !user.hidden ? nil : searchTextOrPassword
+ private func correctPassword(_ user: User, _ pwd: String) -> Bool {
+ if let ph = user.viewPwdHash {
+ return pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash
+ }
+ return false
}
- private func deleteModeButton(_ title: LocalizedStringKey, _ delSMPQueues: Bool) -> some View {
- Button(title, role: .destructive) {
- if let userInfo = userToDelete {
- alert = .deleteUser(userInfo: userInfo, delSMPQueues: delSMPQueues)
+ private func userViewPassword(_ user: User) -> String? {
+ !user.hidden ? nil : searchTextOrPassword
+ }
+
+ @ViewBuilder private func profileActionView(_ action: UserProfileAction) -> some View {
+ let passwordValid = actionPassword == actionPassword.trimmingCharacters(in: .whitespaces)
+ switch action {
+ case let .deleteUser(user, delSMPQueues):
+ let actionEnabled = actionPassword != "" && passwordValid && correctPassword(user, actionPassword)
+ List {
+ Text("Delete user")
+ .font(.title)
+ .bold()
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+ .listRowBackground(Color.clear)
+
+ Section() {
+ ProfilePreview(profileOf: user).padding(.leading, -8)
+ }
+
+ Section {
+ PassphraseField(key: $actionPassword, placeholder: "Profile password", valid: passwordValid)
+ settingsRow("trash") {
+ Button("Delete user", role: .destructive) {
+ profileAction = nil
+ Task { await removeUser(user, delSMPQueues, viewPwd: actionPassword) }
+ }
+ .disabled(!actionEnabled)
+ }
+ } footer: {
+ if actionEnabled {
+ Text("All chats and messages will be deleted - this cannot be undone!")
+ .font(.callout)
+ }
+ }
}
}
}
- private func removeUser(_ userInfo: UserInfo, _ delSMPQueues: Bool) async {
+ private func deleteModeButton(_ title: LocalizedStringKey, _ delSMPQueues: Bool) -> some View {
+ Button(title, role: .destructive) {
+ if let user = userToDelete {
+ if user.hidden && user.activeUser && !correctPassword(user, searchTextOrPassword) {
+ profileAction = .deleteUser(user: user, delSMPQueues: delSMPQueues)
+ } else {
+ alert = .deleteUser(user: user, delSMPQueues: delSMPQueues)
+ }
+ }
+ }
+ }
+
+ private func removeUser(_ user: User, _ delSMPQueues: Bool, viewPwd: String?) async {
do {
- let u = userInfo.user
- if u.activeUser {
+ if user.activeUser {
if let newActive = m.users.first(where: { u in !u.user.activeUser && !u.user.hidden }) {
try await changeActiveUserAsync_(newActive.user.userId, viewPwd: nil)
- try await deleteUser(u)
+ try await deleteUser()
}
} else {
- try await deleteUser(u)
+ try await deleteUser()
}
} catch let error {
let a = getErrorAlert(error, "Error deleting user profile")
alert = .error(title: a.title, error: a.message)
}
- func deleteUser(_ user: User) async throws {
- try await apiDeleteUser(user.userId, delSMPQueues, viewPwd: userViewPassword(user))
+ func deleteUser() async throws {
+ try await apiDeleteUser(user.userId, delSMPQueues, viewPwd: viewPwd)
await MainActor.run { withAnimation { m.removeUser(user) } }
}
}
From 7b33e1fba83c73d46fab49de981a9026d596fb7d Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 29 Mar 2023 17:18:44 +0400
Subject: [PATCH 5/5] core: update cancel file api (#2097)
---
src/Simplex/Chat.hs | 26 ++++++++++++++++----------
src/Simplex/Chat/Controller.hs | 8 ++++----
src/Simplex/Chat/View.hs | 12 ++++++------
3 files changed, 26 insertions(+), 20 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 0fc3693dc2..1102a98b1e 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -1390,8 +1390,9 @@ processChatCommand = \case
CancelFile fileId -> withUser $ \user@User {userId} ->
withChatLock "cancelFile" . procCmd $
withStore (\db -> getFileTransfer db user fileId) >>= \case
- FTSnd ftm@FileTransferMeta {cancelled} fts -> do
- unless cancelled $ do
+ FTSnd ftm@FileTransferMeta {cancelled} fts
+ | cancelled -> throwChatError $ CEFileAlreadyCancelled fileId
+ | otherwise -> do
fileAgentConnIds <- cancelSndFile user ftm fts True
deleteAgentConnectionsAsync user fileAgentConnIds
sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId
@@ -1403,12 +1404,14 @@ processChatCommand = \case
Group gInfo ms <- withStore $ \db -> getGroup db user groupId
void . sendGroupMessage user gInfo ms $ XFileCancel sharedMsgId
_ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer"
- ci <- withStore $ \db -> getChatItemByFileId db user fileId
- pure $ CRSndGroupFileCancelled user ci ftm fts
- FTRcv ftr@RcvFileTransfer {cancelled} -> do
- unless cancelled $
+ ci <- withStore $ \db -> getChatItemByFileId db user fileId
+ pure $ CRSndFileCancelled user ci ftm fts
+ FTRcv ftr@RcvFileTransfer {cancelled}
+ | cancelled -> throwChatError $ CEFileAlreadyCancelled fileId
+ | otherwise -> do
cancelRcvFileTransfer user ftr >>= mapM_ (deleteAgentConnectionAsync user)
- pure $ CRRcvFileCancelled user ftr
+ ci <- withStore $ \db -> getChatItemByFileId db user fileId
+ pure $ CRRcvFileCancelled user ci ftr
FileStatus fileId -> withUser $ \user -> do
fileStatus <- withStore $ \db -> getFileTransferProgress db user fileId
pure $ CRFileTransferStatus user fileStatus
@@ -2819,7 +2822,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
FileChunkCancel ->
unless cancelled $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
- toView $ CRRcvFileSndCancelled user ft
+ ci <- withStore $ \db -> getChatItemByFileId db user fileId
+ toView $ CRRcvFileSndCancelled user ci ft
FileChunk {chunkNo, chunkBytes = chunk} -> do
case integrity of
MsgOk -> pure ()
@@ -3238,7 +3242,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId)
unless cancelled $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
- toView $ CRRcvFileSndCancelled user ft
+ ci <- withStore $ \db -> getChatItemByFileId db user fileId
+ toView $ CRRcvFileSndCancelled user ci ft
xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m ()
xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta = do
@@ -3314,7 +3319,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId)
unless cancelled $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user)
- toView $ CRRcvFileSndCancelled user ft
+ ci <- withStore $ \db -> getChatItemByFileId db user fileId
+ toView $ CRRcvFileSndCancelled user ci ft
else messageError "x.file.cancel: group member attempted to cancel file of another member" -- shouldn't happen now that query includes group member id
(SMDSnd, _) -> messageError "x.file.cancel: group member attempted invalid file cancel"
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 1b13fc34fc..7bd7d6fd4f 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -445,13 +445,12 @@ data ChatResponse
| CRRcvFileStart {user :: User, chatItem :: AChatItem}
| CRRcvFileProgressXFTP {user :: User, chatItem :: AChatItem, receivedSize :: Int64, totalSize :: Int64}
| CRRcvFileComplete {user :: User, chatItem :: AChatItem}
- | CRRcvFileCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
- | CRRcvFileSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
+ | CRRcvFileCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer}
+ | CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer}
| CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
| CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
- | CRSndFileCancelled {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
| CRSndFileRcvCancelled {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
- | CRSndGroupFileCancelled {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]}
+ | CRSndFileCancelled {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]}
| CRSndFileStartXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
| CRSndFileProgressXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sentSize :: Int64, totalSize :: Int64}
| CRSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
@@ -771,6 +770,7 @@ data ChatErrorType
| CEFileNotFound {message :: String}
| CEFileAlreadyReceiving {message :: String}
| CEFileCancelled {message :: String}
+ | CEFileAlreadyCancelled {fileId :: FileTransferId}
| CEFileAlreadyExists {filePath :: FilePath}
| CEFileRead {filePath :: FilePath, message :: String}
| CEFileWrite {filePath :: FilePath, message :: String}
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index a989a09bc5..2352b1e94b 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -137,8 +137,8 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
CRRcvFileProgressXFTP _ _ _ _ -> []
CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci
CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
- CRSndGroupFileCancelled u _ ftm fts -> ttyUser u $ viewSndGroupFileCancelled ftm fts
- CRRcvFileCancelled u ft -> ttyUser u $ receivingFile_ "cancelled" ft
+ CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts
+ CRRcvFileCancelled u _ ft -> ttyUser u $ receivingFile_ "cancelled" ft
CRUserProfileUpdated u p p' -> ttyUser u $ viewUserProfileUpdated p p'
CRContactPrefsUpdated {user = u, fromContact, toContact} -> ttyUser u $ viewUserContactPrefsUpdated u fromContact toContact
CRContactAliasUpdated u c -> ttyUser u $ viewContactAliasUpdated c
@@ -148,10 +148,9 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
CRReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile
CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci
CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci
- CRRcvFileSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
+ CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft
CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
- CRSndFileCancelled _ ft -> sendingFile_ "cancelled" ft
CRSndFileStartXFTP _ _ _ -> []
CRSndFileProgressXFTP _ _ _ _ _ -> []
CRSndFileCompleteXFTP _ _ _ -> []
@@ -1054,8 +1053,8 @@ viewRcvFileSndCancelled :: RcvFileTransfer -> [StyledString]
viewRcvFileSndCancelled ft@RcvFileTransfer {senderDisplayName = c} =
[ttyContact c <> " cancelled sending " <> rcvFile ft]
-viewSndGroupFileCancelled :: FileTransferMeta -> [SndFileTransfer] -> [StyledString]
-viewSndGroupFileCancelled FileTransferMeta {fileId, fileName} fts =
+viewSndFileCancelled :: FileTransferMeta -> [SndFileTransfer] -> [StyledString]
+viewSndFileCancelled FileTransferMeta {fileId, fileName} fts =
case filter (\SndFileTransfer {fileStatus = s} -> s /= FSCancelled && s /= FSComplete) fts of
[] -> ["cancelled sending " <> fileTransferStr fileId fileName]
ts -> ["cancelled sending " <> fileTransferStr fileId fileName <> " to " <> listRecipients ts]
@@ -1274,6 +1273,7 @@ viewChatError logLevel = \case
CEFileNotFound f -> ["file not found: " <> plain f]
CEFileAlreadyReceiving f -> ["file is already being received: " <> plain f]
CEFileCancelled f -> ["file cancelled: " <> plain f]
+ CEFileAlreadyCancelled fileId -> ["file already cancelled: " <> sShow fileId]
CEFileAlreadyExists f -> ["file already exists: " <> plain f]
CEFileRead f e -> ["cannot read file " <> plain f, sShow e]
CEFileWrite f e -> ["cannot write file " <> plain f, sShow e]