From d72c9a6de05d5aa4ea6c379a4139458541c0bc20 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sun, 1 Oct 2023 19:12:17 +0800 Subject: [PATCH 01/17] desktop: ability to always show terminal view (#3074) * desktop: ability to always show terminal view * only show toggle with dev tools enabled --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../chat/simplex/common/model/SimpleXAPI.kt | 2 ++ .../chat/simplex/common/views/TerminalView.kt | 13 ++++++++++++- .../common/views/usersettings/DeveloperView.kt | 11 +++++++++++ .../common/views/usersettings/SettingsView.kt | 9 +++++++++ .../commonMain/resources/MR/base/strings.xml | 1 + .../resources/MR/images/ic_engineering.svg | 1 + .../kotlin/chat/simplex/common/DesktopApp.kt | 17 ++++++++++++++++- 7 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 4bb06afd8b..43043d65fe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -106,6 +106,7 @@ class AppPreferences { val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null) val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) + val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050") private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name) @@ -265,6 +266,7 @@ class AppPreferences { private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" + private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort" private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index e471341669..c4f2a2cbde 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -123,7 +123,18 @@ fun TerminalLog(terminalItems: List) { DisposableEffect(Unit) { onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } } - val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } } + val reversedTerminalItems by remember { + derivedStateOf { + // Such logic prevents concurrent modification + val res = ArrayList() + var i = 0 + while (i < terminalItems.size) { + res.add(terminalItems[i]) + i++ + } + res.asReversed() + } + } val clipboard = LocalClipboardManager.current LazyColumn(state = listState, reverseLayout = true) { items(reversedTerminalItems) { item -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index 68aa1fff9e..a6ac8c14ed 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -34,6 +34,17 @@ fun DeveloperView( ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(it, close) })} SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades) SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools) + if (appPlatform.isDesktop && devTools.value) { + TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked -> + if (checked) { + withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { + m.controller.appPrefs.terminalAlwaysVisible.set(true) + } + } else { + m.controller.appPrefs.terminalAlwaysVisible.set(false) + } + } + } } SectionTextFooter( generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " + diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index 8969e48b2c..eebe7b3f49 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -322,6 +322,15 @@ fun ChatLockItem( } } +@Composable fun TerminalAlwaysVisibleItem(pref: SharedPreference, onChange: (Boolean) -> Unit) { + SettingsActionItemWithContent(painterResource(MR.images.ic_engineering), stringResource(MR.strings.terminal_always_visible), extraPadding = false) { + DefaultSwitch( + checked = remember { pref.state }.value, + onCheckedChange = onChange, + ) + } +} + @Composable fun InstallTerminalAppItem(uriHandler: UriHandler) { SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) { Icon( diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 26e9948d6c..d57211f4ac 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1056,6 +1056,7 @@ Database downgrade Incompatible database version Confirm database upgrades + Show console in new window Invalid migration confirmation Upgrade and open chat Downgrade and open chat diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg new file mode 100644 index 0000000000..0848537041 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 62a4d9e214..6b81209d4c 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -14,10 +14,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.defaultLocale import chat.simplex.common.platform.desktopPlatform +import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.views.TerminalView import chat.simplex.common.views.helpers.FileDialogChooser +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import java.awt.event.WindowEvent import java.awt.event.WindowFocusListener @@ -118,6 +121,18 @@ fun showApp() = application { } } } + // Reload all strings in all @Composable's after language change at runtime + if (remember { ChatController.appPrefs.terminalAlwaysVisible.state }.value && remember { ChatController.appPrefs.appLanguage.state }.value != "") { + var hiddenUntilRestart by remember { mutableStateOf(false) } + if (!hiddenUntilRestart) { + val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH, height = 768.dp) + Window(state = cWindowState, onCloseRequest = ::exitApplication, title = stringResource(MR.strings.chat_console)) { + SimpleXTheme { + TerminalView(ChatModel) { hiddenUntilRestart = true } + } + } + } + } } class SimplexWindowState { From 968d8e9c343b881111f06331d4d251ce75e7bdb9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 1 Oct 2023 13:19:32 +0100 Subject: [PATCH 02/17] core: 5.4.0.0 --- package.yaml | 2 +- simplex-chat.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 406f9aabaa..907a2a0686 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.3.1.0 +version: 5.4.0.0 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/simplex-chat.cabal b/simplex-chat.cabal index dc3a23adc0..ac641f841e 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 5.3.1.0 +version: 5.4.0.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 695d47da2db6b793e1fe7866733825221c9f4f18 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sun, 1 Oct 2023 20:33:15 +0800 Subject: [PATCH 03/17] desktop: Windows build (#3143) * desktop: Windows build * temp * temp * new way of libs loading * new way of libs loading * Revert "new way of libs loading" This reverts commit 8632f8a8f74ae5b1fd1a3aad8662734a96510a01. * made VLC working on Windows * unused lib * scripts * updated script * fix path * fix lib loading * fix lib loading * packaging options * different file manager implementation on Windows --------- Co-authored-by: Avently Co-authored-by: avently Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../src/commonMain/cpp/desktop/CMakeLists.txt | 13 +++++--- .../common/platform/Platform.desktop.kt | 1 + .../common/platform/RecAndPlay.desktop.kt | 6 ++-- .../views/helpers/DefaultDialog.desktop.kt | 8 ++--- .../common/views/helpers/Utils.desktop.kt | 2 +- apps/multiplatform/desktop/build.gradle.kts | 13 ++++---- .../kotlin/chat/simplex/desktop/Main.kt | 30 +++++++++++++++++-- scripts/android/download-libs.sh | 2 +- scripts/desktop/build-lib-windows.sh | 22 ++++++++++++++ scripts/desktop/download-lib-windows.sh | 27 +++++++++++++++++ scripts/desktop/prepare-vlc-windows.sh | 25 ++++++++++++++++ 11 files changed, 127 insertions(+), 22 deletions(-) create mode 100755 scripts/desktop/build-lib-windows.sh create mode 100644 scripts/desktop/download-lib-windows.sh create mode 100644 scripts/desktop/prepare-vlc-windows.sh diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt index 09ef6fd533..eb4794dd6a 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt @@ -31,9 +31,9 @@ else() set(CMAKE_BUILD_RPATH "@loader_path") endif() -if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "amd64") +if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "amd64" OR ${CMAKE_SYSTEM_PROCESSOR} MATCHES "AMD64") set(OS_LIB_ARCH "x86_64") -elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm64") +elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm64" OR ${CMAKE_SYSTEM_PROCESSOR} MATCHES "ARM64") set(OS_LIB_ARCH "aarch64") else() set(OS_LIB_ARCH "${CMAKE_SYSTEM_PROCESSOR}") @@ -55,8 +55,13 @@ add_library( # Sets the name of the library. add_library( simplex SHARED IMPORTED ) # Lib has different name because of version, find it -FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libHSsimplex-chat-*.${OS_LIB_EXT}) -set_target_properties( simplex PROPERTIES IMPORTED_LOCATION ${SIMPLEXLIB}) +FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex*.${OS_LIB_EXT}) + +if(WIN32) + set_target_properties( simplex PROPERTIES IMPORTED_IMPLIB ${SIMPLEXLIB}) +else() + set_target_properties( simplex PROPERTIES IMPORTED_LOCATION ${SIMPLEXLIB}) +endif() # Specifies libraries CMake should link to your target library. You diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt index 2121b9cfdf..cb4e3acdb4 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt @@ -16,6 +16,7 @@ enum class DesktopPlatform(val libPath: String, val libExtension: String, val co MAC_AARCH64("/libs/mac-aarch64", "dylib", unixConfigPath, unixDataPath); fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64 + fun isWindows() = this == WINDOWS_X86_64 fun isMac() = this == MAC_X86_64 || this == MAC_AARCH64 } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt index ed8efcd57f..4439680c66 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt @@ -54,9 +54,9 @@ actual object AudioPlayer: AudioPlayerInterface { if (fileSource.cryptoArgs != null) { val tmpFile = fileSource.createTmpFileIfNeeded() decryptCryptoFile(absoluteFilePath, fileSource.cryptoArgs, tmpFile.absolutePath) - player.media().prepare("file://${tmpFile.absolutePath}") + player.media().prepare(tmpFile.toURI().toString().replaceFirst("file:", "file://")) } else { - player.media().prepare("file://$absoluteFilePath") + player.media().prepare(File(absoluteFilePath).toURI().toString().replaceFirst("file:", "file://")) } }.onFailure { Log.e(TAG, it.stackTraceToString()) @@ -171,7 +171,7 @@ actual object AudioPlayer: AudioPlayerInterface { var res: Int? = null try { val helperPlayer = AudioPlayerComponent().mediaPlayer() - helperPlayer.media().startPaused("file://$unencryptedFilePath") + helperPlayer.media().startPaused(File(unencryptedFilePath).toURI().toString().replaceFirst("file:", "file://")) res = helperPlayer.duration helperPlayer.stop() helperPlayer.release() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt index 20675dc74e..79fcda7a50 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt @@ -10,12 +10,12 @@ import androidx.compose.ui.input.key.* import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import chat.simplex.common.DialogParams +import chat.simplex.common.platform.desktopPlatform import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.awt.FileDialog import java.io.File -import java.util.* import javax.swing.JFileChooser import javax.swing.filechooser.FileFilter import javax.swing.filechooser.FileNameExtensionFilter @@ -53,7 +53,7 @@ fun FrameWindowScope.FileDialogChooser( params: DialogParams, onResult: (result: List) -> Unit ) { - if (isLinux()) { + if (desktopPlatform.isLinux() || desktopPlatform.isWindows()) { FileDialogChooserMultiple(title, isLoad, params.filename, params.allowMultiple, params.fileFilter, params.fileFilterDescription, onResult) } else { FileDialogAwt(title, isLoad, params.filename, params.allowMultiple, params.fileFilter, onResult) @@ -121,7 +121,7 @@ fun FrameWindowScope.FileDialogChooserMultiple( } /* -* Has graphic glitches on many Linux distributions, so use only on non-Linux systems +* Has graphic glitches on many Linux distributions, so use only on non-Linux systems. Also file filter doesn't work on Windows * */ @Composable private fun FrameWindowScope.FileDialogAwt( @@ -159,5 +159,3 @@ private fun FrameWindowScope.FileDialogAwt( }, dispose = FileDialog::dispose ) - -fun isLinux(): Boolean = System.getProperty("os.name", "generic").lowercase(Locale.ENGLISH) == "linux" diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index 21b2cfa6e0..2a9042c433 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -88,7 +88,7 @@ actual fun escapedHtmlToAnnotatedString(text: String, density: Density): Annotat } actual fun getAppFileUri(fileName: String): URI = - URI("file:" + appFilesDir.absolutePath + File.separator + fileName) + URI(appFilesDir.toURI().toString() + "/" + fileName) actual fun getLoadedImage(file: CIFile?): Pair? { val filePath = getLoadedFilePath(file) diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index a4e8207a15..bd9cb68d89 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -63,8 +63,8 @@ compose { windows { packageName = "SimpleX" iconFile.set(project.file("src/jvmMain/resources/distribute/simplex.ico")) - console = true - perUserInstall = true + console = false + perUserInstall = false dirChooser = true } macOS { @@ -119,9 +119,9 @@ cmake { /*machines.customMachines.register("linux-aarch64") { toolchainFile.set(project.file("$cppPath/toolchains/aarch64-linux-gnu-gcc.cmake")) }*/ - machines.customMachines.register("win-amd64") { + /*machines.customMachines.register("win-amd64") { toolchainFile.set(project.file("$cppPath/toolchains/x86_64-windows-mingw32-gcc.cmake")) - } + }*/ if (machines.host.name == "mac-amd64") { machines.customMachines.register("mac-amd64") { toolchainFile.set(project.file("$cppPath/toolchains/x86_64-mac-apple-darwin-gcc.cmake")) @@ -139,6 +139,9 @@ cmake { val main by creating { cmakeLists.set(file("$cppPath/desktop/CMakeLists.txt")) targetMachines.addAll(compileMachineTargets.toSet()) + if (machines.host.name.contains("win")) { + cmakeArgs.add("-G MinGW Makefiles") + } } } } @@ -191,7 +194,7 @@ afterEvaluate { copyIfNeeded(destinationDir, copyDetails) } copy { - from("${project(":desktop").buildDir}/cmake/main/win-amd64", "$cppPath/desktop/libs/windows-x86_64", "$cppPath/desktop/libs/windows-x86_64/deps") + from("${project(":desktop").buildDir}/cmake/main/windows-amd64", "$cppPath/desktop/libs/windows-x86_64", "$cppPath/desktop/libs/windows-x86_64/deps") into("src/jvmMain/resources/libs/windows-x86_64") include("*.dll") eachFile { diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index 72c41a665b..a0be877329 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -18,13 +18,15 @@ fun main() { @Suppress("UnsafeDynamicallyLoadedCode") private fun initHaskell() { - val libApp = "libapp-lib.${desktopPlatform.libExtension}" val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs") copyResources(desktopPlatform.libPath, libsTmpDir.toPath()) - System.load(File(libsTmpDir, libApp).absolutePath) - vlcDir.deleteRecursively() Files.move(File(libsTmpDir, "vlc").toPath(), vlcDir.toPath(), StandardCopyOption.REPLACE_EXISTING) + if (desktopPlatform == DesktopPlatform.WINDOWS_X86_64) { + windowsLoadRequiredLibs(libsTmpDir) + } else { + System.load(File(libsTmpDir, "libapp-lib.${desktopPlatform.libExtension}").absolutePath) + } // No picture without preloading it, only sound. However, with libs from AppImage it works without preloading //val libXcb = "libvlc_xcb_events.so.0.0.0" //System.load(File(File(vlcDir, "vlc"), libXcb).absolutePath) @@ -55,3 +57,25 @@ private fun copyResources(from: String, to: Path) { } }) } + +private fun windowsLoadRequiredLibs(libsTmpDir: File) { + val mainLibs = arrayOf( + "libcrypto-3-x64.dll", + "libffi-8.dll", + "libgmp-10.dll", + "libsimplex.dll", + "libapp-lib.dll" + ) + mainLibs.forEach { + System.load(File(libsTmpDir, it).absolutePath) + } + val vlcLibs = arrayOf( + "libvlccore.dll", + "libvlc.dll", + "axvlc.dll", + "npvlc.dll" + ) + vlcLibs.forEach { + System.load(File(vlcDir, it).absolutePath) + } +} diff --git a/scripts/android/download-libs.sh b/scripts/android/download-libs.sh index f67cc397a7..4702f03600 100755 --- a/scripts/android/download-libs.sh +++ b/scripts/android/download-libs.sh @@ -7,7 +7,7 @@ function readlink() { } if [ -z "${1}" ]; then - echo "Job repo is unset. Provide it via first argument like: $(readlink "$0")/download_libs.sh https://something.com/job/something/{master,stable}" + echo "Job repo is unset. Provide it via first argument like: $(readlink "$0")/download-libs.sh https://something.com/job/something/{master,stable}" exit 1 fi diff --git a/scripts/desktop/build-lib-windows.sh b/scripts/desktop/build-lib-windows.sh new file mode 100755 index 0000000000..658324baa8 --- /dev/null +++ b/scripts/desktop/build-lib-windows.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +function readlink() { + echo "$(cd "$(dirname "$1")"; pwd -P)" +} +root_dir="$(dirname "$(dirname "$(readlink "$0")")")" + +OS=windows +ARCH=`uname -a | rev | cut -d' ' -f2 | rev` +JOB_REPO=${1:-$SIMPLEX_CI_REPO_URL} + +cd $root_dir + +rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/ +rm -rf apps/multiplatform/desktop/build/cmake + +mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +scripts/desktop/download-lib-windows.sh $JOB_REPO +scripts/desktop/prepare-vlc-windows.sh diff --git a/scripts/desktop/download-lib-windows.sh b/scripts/desktop/download-lib-windows.sh new file mode 100644 index 0000000000..14439274c0 --- /dev/null +++ b/scripts/desktop/download-lib-windows.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +function readlink() { + echo "$(cd "$(dirname "$1")"; pwd -P)" +} + +if [ -z "${1}" ]; then + echo "Job repo is unset. Provide it via first argument like: $(readlink "$0")/download-lib-windows.sh https://something.com/job/something/{windows,windows-8107}" + exit 1 +fi + +job_repo=$1 +arch=x86_64 +root_dir="$(dirname "$(dirname "$(readlink "$0")")")" +output_dir="$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-$arch/" + +mkdir -p "$output_dir"/deps 2> /dev/null + +curl --location -o libsimplex.zip $job_repo/$arch-linux.$arch-windows:lib:simplex-chat/latest/download/1 && \ +$WINDIR\\System32\\tar.exe -xf libsimplex.zip && \ +mv libsimplex.dll "$output_dir" && \ +mv libcrypto*.dll "$output_dir/deps" && \ +mv libffi*.dll "$output_dir/deps" && \ +mv libgmp*.dll "$output_dir/deps" && \ +rm libsimplex.zip diff --git a/scripts/desktop/prepare-vlc-windows.sh b/scripts/desktop/prepare-vlc-windows.sh new file mode 100644 index 0000000000..bdb492344a --- /dev/null +++ b/scripts/desktop/prepare-vlc-windows.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -e + +function readlink() { + echo "$(cd "$(dirname "$1")"; pwd -P)" +} +root_dir="$(dirname "$(dirname "$(readlink "$0")")")" +vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-x86_64/deps/vlc +rm -rf $vlc_dir +mkdir -p $vlc_dir/vlc || exit 0 + +cd /tmp +mkdir tmp 2>/dev/null || true +cd tmp +curl https://irltoolkit.mm.fcix.net/videolan-ftp/vlc/3.0.18/win64/vlc-3.0.18-win64.zip -L -o vlc +$WINDIR\\System32\\tar.exe -xf vlc +cd vlc-* +# Setting the same date as the date that will be on the file after extraction from JAR to make VLC cache checker happy +find plugins | grep ".dll" | xargs touch -m -d "1970-01-01T00:00:00Z" +./vlc-cache-gen plugins +cp *.dll $vlc_dir/ +cp -r -p plugins/ $vlc_dir/vlc/plugins +cd ../../ +rm -rf tmp From 7231201c3c9445d998c8324a96eab7a5c791f03b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 1 Oct 2023 18:31:52 +0100 Subject: [PATCH 04/17] v5.4-beta.0: ios 176, android 156, desktop 12 * desktop: v5.4-beta.0 build 12 * v5.4-beta.0: ios 176, android 156, desktop 12 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 64 +++++++++++----------- apps/multiplatform/gradle.properties | 8 +-- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 20ba15e925..8b45e45f95 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -48,11 +48,6 @@ 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; }; 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */; }; 5C55A92E283D0FDE00C4E99E /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = 5C55A92D283D0FDE00C4E99E /* sounds */; }; - 5C56251A2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625152AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a */; }; - 5C56251B2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625162AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a */; }; - 5C56251C2AC1DE5900A21210 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625172AC1DE5900A21210 /* libgmpxx.a */; }; - 5C56251D2AC1DE5900A21210 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625182AC1DE5900A21210 /* libgmp.a */; }; - 5C56251E2AC1DE5900A21210 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625192AC1DE5900A21210 /* libffi.a */; }; 5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; }; 5C58BCD6292BEBE600AF9E4F /* CIChatFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */; }; 5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */; }; @@ -119,6 +114,11 @@ 5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; }; 5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; }; 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; + 5CC7398D2AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739882AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a */; }; + 5CC7398E2AC9D168009470A9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC739892AC9D168009470A9 /* libgmp.a */; }; + 5CC7398F2AC9D168009470A9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC7398A2AC9D168009470A9 /* libffi.a */; }; + 5CC739902AC9D168009470A9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC7398B2AC9D168009470A9 /* libgmpxx.a */; }; + 5CC739912AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC7398C2AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; @@ -293,11 +293,6 @@ 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = ""; }; 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = ""; }; 5C55A92D283D0FDE00C4E99E /* sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = sounds; sourceTree = ""; }; - 5C5625152AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a"; sourceTree = ""; }; - 5C5625162AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a"; sourceTree = ""; }; - 5C5625172AC1DE5900A21210 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5C5625182AC1DE5900A21210 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C5625192AC1DE5900A21210 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = ""; }; 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIChatFeatureView.swift; sourceTree = ""; }; 5C5B67912ABAF4B500DA9412 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; @@ -400,6 +395,11 @@ 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; }; 5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; + 5CC739882AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a"; sourceTree = ""; }; + 5CC739892AC9D168009470A9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CC7398A2AC9D168009470A9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5CC7398B2AC9D168009470A9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CC7398C2AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a"; sourceTree = ""; }; 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; }; 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; }; @@ -507,12 +507,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5CC739902AC9D168009470A9 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C56251C2AC1DE5900A21210 /* libgmpxx.a in Frameworks */, - 5C56251B2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a in Frameworks */, - 5C56251A2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a in Frameworks */, - 5C56251E2AC1DE5900A21210 /* libffi.a in Frameworks */, - 5C56251D2AC1DE5900A21210 /* libgmp.a in Frameworks */, + 5CC7398D2AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a in Frameworks */, + 5CC7398E2AC9D168009470A9 /* libgmp.a in Frameworks */, + 5CC739912AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a in Frameworks */, + 5CC7398F2AC9D168009470A9 /* libffi.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -574,11 +574,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C5625192AC1DE5900A21210 /* libffi.a */, - 5C5625182AC1DE5900A21210 /* libgmp.a */, - 5C5625172AC1DE5900A21210 /* libgmpxx.a */, - 5C5625152AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a */, - 5C5625162AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a */, + 5CC7398A2AC9D168009470A9 /* libffi.a */, + 5CC739892AC9D168009470A9 /* libgmp.a */, + 5CC7398B2AC9D168009470A9 /* libgmpxx.a */, + 5CC739882AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp-ghc8.10.7.a */, + 5CC7398C2AC9D168009470A9 /* libHSsimplex-chat-5.4.0.0-EcKpK3v0NXRK7pgDye4kqp.a */, ); path = Libraries; sourceTree = ""; @@ -1486,7 +1486,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 174; + CURRENT_PROJECT_VERSION = 176; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1507,7 +1507,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.3.1; + MARKETING_VERSION = 5.4; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1528,7 +1528,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 174; + CURRENT_PROJECT_VERSION = 176; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1549,7 +1549,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.3.1; + MARKETING_VERSION = 5.4; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1608,7 +1608,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 174; + CURRENT_PROJECT_VERSION = 176; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1621,7 +1621,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.3.1; + MARKETING_VERSION = 5.4; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1640,7 +1640,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 174; + CURRENT_PROJECT_VERSION = 176; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1653,7 +1653,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.3.1; + MARKETING_VERSION = 5.4; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1672,7 +1672,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 174; + CURRENT_PROJECT_VERSION = 176; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1696,7 +1696,7 @@ "$(inherited)", "$(PROJECT_DIR)/Libraries/sim", ); - MARKETING_VERSION = 5.3.1; + MARKETING_VERSION = 5.4; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1718,7 +1718,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 174; + CURRENT_PROJECT_VERSION = 176; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1742,7 +1742,7 @@ "$(inherited)", "$(PROJECT_DIR)/Libraries/sim", ); - MARKETING_VERSION = 5.3.1; + MARKETING_VERSION = 5.4; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 0d047a7917..e37dbca8eb 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -25,11 +25,11 @@ android.nonTransitiveRClass=true android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 -android.version_name=5.3.1 -android.version_code=154 +android.version_name=5.4-beta.0 +android.version_code=156 -desktop.version_name=5.3.1 -desktop.version_code=11 +desktop.version_name=5.4-beta.0 +desktop.version_code=12 kotlin.version=1.8.20 gradle.plugin.version=7.4.2 From 157ea59ebb9320f0b69132f24d033e3877b4951b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 1 Oct 2023 18:53:58 +0100 Subject: [PATCH 05/17] docs: update downloads page --- docs/DOWNLOADS.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index 810fd6f4c1..64b76e7fec 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -1,13 +1,15 @@ --- title: Download SimpleX apps permalink: /downloads/index.html -revision: 20.09.2023 +revision: 01.10.2023 --- -| Updated 20.09.2023 | Languages: EN | +| Updated 01.10.2023 | Languages: EN | # Download SimpleX apps -The latest version is v5.3.1. +The latest stable version is v5.3.1. + +You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). - [desktop](#desktop-app) - [mobile](#mobile-apps) @@ -23,7 +25,7 @@ Using the same profile as on mobile device is not yet supported – you need to **Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-macos-aarch64.dmg) (Apple Silicon). -**Windows**: coming soon. +**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0-beta.0/simplex-desktop-windows-x86-64.msi) (BETA). ## Mobile apps From 8545a1e8f93a396dc05ab9722944cc501bfc577c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 1 Oct 2023 20:46:30 +0100 Subject: [PATCH 06/17] ci: make docs update rebuild website --- .github/workflows/web.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 4525f3c5dc..039c136464 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -9,6 +9,7 @@ on: - website/** - images/** - blog/** + - docs/** - .github/workflows/web.yml jobs: From d4cbef1ba1728456e6825c5866effc0eff8383ab Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:29:13 +0400 Subject: [PATCH 07/17] core: notify about contact deletion only if contact is ready, catch errors (#3160) --- src/Simplex/Chat.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 2a568d628f..5b56c0ac84 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -897,7 +897,8 @@ processChatCommand = \case filesInfo <- withStore' $ \db -> getContactFileInfo db user ct withChatLock "deleteChat direct" . procCmd $ do deleteFilesAndConns user filesInfo - when (contactActive ct && notify) . void $ sendDirectContactMessage ct XDirectDel + when (isReady ct && contactActive ct && notify) $ + void (sendDirectContactMessage ct XDirectDel) `catchChatError` const (pure ()) contactConnIds <- map aConnId <$> withStore (\db -> getContactConnections db userId ct) deleteAgentConnectionsAsync user contactConnIds -- functions below are called in separate transactions to prevent crashes on android From 0d93dab6927837faa412f557fb082575910dedb6 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 2 Oct 2023 22:46:30 +0800 Subject: [PATCH 08/17] android, desktop: added useful logs (#3163) --- .../kotlin/chat/simplex/common/model/ChatModel.kt | 7 +++++++ .../kotlin/chat/simplex/common/views/chat/ChatView.kt | 10 +++++++++- .../common/views/chatlist/ChatListNavLinkView.kt | 2 ++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 0eb02515b5..8687ac390a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -224,6 +224,7 @@ object ChatModel { } // add to current chat if (chatId.value == cInfo.id) { + Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") withContext(Dispatchers.Main) { // Prevent situation when chat item already in the list received from backend if (chatItems.none { it.id == cItem.id }) { @@ -231,6 +232,7 @@ object ChatModel { chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem) } else { chatItems.add(cItem) + Log.d(TAG, "TODOCHAT: addChatItem: added to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") } } } @@ -259,13 +261,16 @@ object ChatModel { } // update current chat return if (chatId.value == cInfo.id) { + Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") withContext(Dispatchers.Main) { val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } if (itemIndex >= 0) { chatItems[itemIndex] = cItem + Log.d(TAG, "TODOCHAT: upsertChatItem: updated in chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") false } else { chatItems.add(cItem) + Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") true } } @@ -374,6 +379,7 @@ object ChatModel { var markedRead = 0 if (chatId.value == cInfo.id) { var i = 0 + Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marking read ${cInfo.id}, current chatId ${chatId.value}, size was ${chatItems.size}") while (i < chatItems.count()) { val item = chatItems[i] if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) { @@ -388,6 +394,7 @@ object ChatModel { } i += 1 } + Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marked read ${cInfo.id}, current chatId ${chatId.value}, size now ${chatItems.size}") } return markedRead } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index b22b2f91c8..ee9e109a7b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -66,11 +66,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: launch { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() + .onEach { Log.d(TAG, "TODOCHAT: chatId: activeChatId ${activeChat.value?.id} == new chatId $it ${activeChat.value?.id == it} ") } .filter { it != null && activeChat.value?.id != it } .collect { chatId -> // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly // Also for situation when chatId changes after clicking in notification, etc activeChat.value = chatModel.getChat(chatId!!) + Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}") markUnreadChatAsRead(activeChat, chatModel) } } @@ -89,9 +91,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: } } .distinctUntilChanged() + .onEach { Log.d(TAG, "TODOCHAT: chats: activeChatId ${activeChat.value?.id} == new chatId ${it?.id} ${activeChat.value?.id == it?.id} ") } // Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions .filter { it != null && it?.chatInfo != activeChat.value?.chatInfo } - .collect { activeChat.value = it } + .collect { + activeChat.value = it + Log.d(TAG, "TODOCHAT: chats: activeChatId became ${activeChat.value?.id}") + } } } val view = LocalMultiplatformView() @@ -218,7 +224,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: val firstId = chatModel.chatItems.firstOrNull()?.id if (c != null && firstId != null) { withApi { + Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}") apiLoadPrevMessages(c.chatInfo, chatModel, firstId, searchText.value) + Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}") } } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 7a0fc12786..4d95431a37 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -122,9 +122,11 @@ suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) { } suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) { + Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}") val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId) if (chat != null) { openChat(chat, chatModel) + Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}") } } From fdf3da73aadafe1e94313da35fbc6088e6a616f6 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 3 Oct 2023 00:23:38 +0800 Subject: [PATCH 09/17] desktop: making chat list item to have a hover effect (#3162) * desktop: making chat list item to have a hover effect * changes * fix --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../chatlist/ChatListNavLinkView.android.kt | 39 ++++++++++++ .../views/chatlist/ChatListNavLinkView.kt | 56 ++++++++--------- .../chatlist/ChatListNavLinkView.desktop.kt | 60 +++++++++++++++++++ 3 files changed, 124 insertions(+), 31 deletions(-) create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt create mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt new file mode 100644 index 0000000000..3f33913e54 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt @@ -0,0 +1,39 @@ +package chat.simplex.common.views.chatlist + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.Divider +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import chat.simplex.common.views.helpers.* + +@Composable +actual fun ChatListNavLinkLayout( + chatLinkPreview: @Composable () -> Unit, + click: () -> Unit, + dropdownMenuItems: (@Composable () -> Unit)?, + showMenu: MutableState, + stopped: Boolean, + selectedChat: State +) { + var modifier = Modifier.fillMaxWidth() + if (!stopped) modifier = modifier + .combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) + .onRightClick { showMenu.value = true } + Box(modifier) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp, top = 8.dp, end = 12.dp, bottom = 8.dp), + verticalAlignment = Alignment.Top + ) { + chatLinkPreview() + } + if (dropdownMenuItems != null) { + DefaultDropdownMenu(showMenu, dropdownMenuItems = dropdownMenuItems) + } + } + Divider(Modifier.padding(horizontal = 8.dp)) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 4d95431a37..41c94f21d9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -1,7 +1,6 @@ package chat.simplex.common.views.chatlist import SectionItemView -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -14,6 +13,10 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -44,6 +47,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { showMenu.value = false delay(500L) } + val selectedChat = remember(chat.id) { derivedStateOf { chat.id == ChatModel.chatId.value } } val showChatPreviews = chatModel.showChatPreviews.value when (chat.chatInfo) { is ChatInfo.Direct -> { @@ -53,7 +57,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { click = { directChatAction(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) }, showMenu, - stopped + stopped, + selectedChat ) } is ChatInfo.Group -> @@ -62,7 +67,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) }, dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) }, showMenu, - stopped + stopped, + selectedChat ) is ChatInfo.ContactRequest -> ChatListNavLinkLayout( @@ -70,7 +76,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { click = { contactRequestAlertDialog(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactRequestMenuItems(chat.chatInfo, chatModel, showMenu) }, showMenu, - stopped + stopped, + selectedChat ) is ChatInfo.ContactConnection -> ChatListNavLinkLayout( @@ -84,7 +91,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { }, dropdownMenuItems = { ContactConnectionMenuItems(chat.chatInfo, chatModel, showMenu) }, showMenu, - stopped + stopped, + selectedChat ) is ChatInfo.InvalidJSON -> ChatListNavLinkLayout( @@ -97,7 +105,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { }, dropdownMenuItems = null, showMenu, - stopped + stopped, + selectedChat ) } } @@ -630,32 +639,14 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo } @Composable -fun ChatListNavLinkLayout( +expect fun ChatListNavLinkLayout( chatLinkPreview: @Composable () -> Unit, click: () -> Unit, dropdownMenuItems: (@Composable () -> Unit)?, showMenu: MutableState, - stopped: Boolean -) { - var modifier = Modifier.fillMaxWidth() - if (!stopped) modifier = modifier - .combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) - .onRightClick { showMenu.value = true } - Box(modifier) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 8.dp, top = 8.dp, end = 12.dp, bottom = 8.dp), - verticalAlignment = Alignment.Top - ) { - chatLinkPreview() - } - if (dropdownMenuItems != null) { - DefaultDropdownMenu(showMenu, dropdownMenuItems = dropdownMenuItems) - } - } - Divider(Modifier.padding(horizontal = 8.dp)) -} + stopped: Boolean, + selectedChat: State +) @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, @@ -692,7 +683,8 @@ fun PreviewChatListNavLinkDirect() { click = {}, dropdownMenuItems = null, showMenu = remember { mutableStateOf(false) }, - stopped = false + stopped = false, + selectedChat = remember { mutableStateOf(false) } ) } } @@ -732,7 +724,8 @@ fun PreviewChatListNavLinkGroup() { click = {}, dropdownMenuItems = null, showMenu = remember { mutableStateOf(false) }, - stopped = false + stopped = false, + selectedChat = remember { mutableStateOf(false) } ) } } @@ -752,7 +745,8 @@ fun PreviewChatListNavLinkContactRequest() { click = {}, dropdownMenuItems = null, showMenu = remember { mutableStateOf(false) }, - stopped = false + stopped = false, + selectedChat = remember { mutableStateOf(false) } ) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt new file mode 100644 index 0000000000..0ad69d1c04 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt @@ -0,0 +1,60 @@ +package chat.simplex.common.views.chatlist + +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.unit.dp +import chat.simplex.common.views.helpers.* + +private object NoIndication : Indication { + private object NoIndicationInstance : IndicationInstance { + override fun ContentDrawScope.drawIndication() { + drawContent() + } + } + @Composable + override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { + return NoIndicationInstance + } +} + +@Composable +actual fun ChatListNavLinkLayout( + chatLinkPreview: @Composable () -> Unit, + click: () -> Unit, + dropdownMenuItems: (@Composable () -> Unit)?, + showMenu: MutableState, + stopped: Boolean, + selectedChat: State +) { + var modifier = Modifier.fillMaxWidth() + if (!stopped) modifier = modifier + .background(color = if (selectedChat.value) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified) + .combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) + .onRightClick { showMenu.value = true } + CompositionLocalProvider( + LocalIndication provides if (selectedChat.value && !stopped) NoIndication else LocalIndication.current + ) { + Box(modifier) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 8.dp, top = 8.dp, end = 12.dp, bottom = 8.dp), + verticalAlignment = Alignment.Top + ) { + chatLinkPreview() + } + if (dropdownMenuItems != null) { + DefaultDropdownMenu(showMenu, dropdownMenuItems = dropdownMenuItems) + } + } + } + Divider() +} From 77db70139b428e608aaaeac8be88a73cf4974c84 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 3 Oct 2023 00:25:49 +0800 Subject: [PATCH 10/17] windows: shortcut for installator (#3156) Co-authored-by: avently --- apps/multiplatform/desktop/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index bd9cb68d89..3da87850d5 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -66,6 +66,7 @@ compose { console = false perUserInstall = false dirChooser = true + shortcut = true } macOS { packageName = "SimpleX" From da2a94578abdf82f1650680ef9a72d099474a0eb Mon Sep 17 00:00:00 2001 From: zenobit Date: Mon, 2 Oct 2023 22:21:26 +0200 Subject: [PATCH 11/17] typo (#3121) --- ...-v5-3-desktop-app-local-file-encryption-directory-service.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md b/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md index 8928e9724d..fc924a8706 100644 --- a/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md +++ b/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md @@ -96,7 +96,7 @@ Incognito mode was [added a year ago](./20220901-simplex-chat-v3.2-incognito-mod It is now simpler to use - you can decide whether to connect to a contact or join a group using your main profile at a point when you create an invitation link or connect via a link or QR code. -When you are connecting to people your know you usually want to share your main profile, and when connecting to public groups or strangers, you may prefer to use a random profile. +When you are connecting to people you know you usually want to share your main profile, and when connecting to public groups or strangers, you may prefer to use a random profile. ## SimpleX platform From 38be27271f48c71ac5bc5097531f0411bffc8bc3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 2 Oct 2023 21:56:11 +0100 Subject: [PATCH 12/17] core: profile names with spaces (#3105) * core: profile names with spaces * test * more test * update name validation, export C API * refactor * validate name when creating profile in CLI * validate display name in all APIs when it is changed --- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 52 +++++++++--- src/Simplex/Chat/Controller.hs | 1 + src/Simplex/Chat/Mobile.hs | 6 ++ src/Simplex/Chat/View.hs | 143 ++++++++++++++++++--------------- tests/ChatTests/Profiles.hs | 75 ++++++++++++++++- tests/ChatTests/Utils.hs | 2 +- tests/MobileTests.hs | 10 +++ tests/Test.hs | 2 + tests/ValidNames.hs | 27 +++++++ 10 files changed, 237 insertions(+), 82 deletions(-) create mode 100644 tests/ValidNames.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index ac641f841e..0b613310f7 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -456,6 +456,7 @@ test-suite simplex-chat-test MobileTests ProtocolTests SchemaDump + ValidNames ViewTests WebRTCTests Broadcast.Bot diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 5b56c0ac84..5d370ee651 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -32,7 +32,7 @@ import Data.Bifunctor (bimap, first) import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Char (isSpace, toLower) +import Data.Char import Data.Constraint (Dict (..)) import Data.Either (fromRight, rights) import Data.Fixed (div') @@ -359,6 +359,7 @@ processChatCommand :: forall m. ChatMonad m => ChatCommand -> m ChatResponse processChatCommand = \case ShowActiveUser -> withUser' $ pure . CRActiveUser CreateActiveUser NewUser {profile, sameServers, pastTimestamp} -> do + forM_ profile $ \Profile {displayName} -> checkValidName displayName p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile u <- asks currentUser (smp, smpServers) <- chooseServers SPSMP @@ -1457,7 +1458,8 @@ processChatCommand = \case chatRef <- getChatRef user chatName chatItemId <- getChatItemIdByText user chatRef msg processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction - APINewGroup userId gProfile -> withUserId userId $ \user -> do + APINewGroup userId gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do + checkValidName displayName gVar <- asks idsDrg groupInfo <- withStore $ \db -> createNewGroup db gVar user gProfile pure $ CRGroupCreated user groupInfo @@ -1962,9 +1964,10 @@ processChatCommand = \case updateProfile :: User -> Profile -> m ChatResponse updateProfile user p' = updateProfile_ user p' $ withStore $ \db -> updateUserProfile db user p' updateProfile_ :: User -> Profile -> m User -> m ChatResponse - updateProfile_ user@User {profile = p} p' updateUser + updateProfile_ user@User {profile = p@LocalProfile {displayName = n}} p'@Profile {displayName = n'} updateUser | p' == fromLocalProfile p = pure $ CRUserProfileNoChange user | otherwise = do + when (n /= n') $ checkValidName n' -- read contacts before user update to correctly merge preferences -- [incognito] filter out contacts with whom user has incognito connections contacts <- @@ -2006,8 +2009,9 @@ processChatCommand = \case when (directOrUsed ct') $ createSndFeatureItems user ct ct' pure $ CRContactPrefsUpdated user ct ct' runUpdateGroupProfile :: User -> Group -> GroupProfile -> m ChatResponse - runUpdateGroupProfile user (Group g@GroupInfo {groupProfile = p} ms) p' = do + runUpdateGroupProfile user (Group g@GroupInfo {groupProfile = p@GroupProfile {displayName = n}} ms) p'@GroupProfile {displayName = n'} = do assertUserGroupRole g GROwner + when (n /= n') $ checkValidName n' g' <- withStore $ \db -> updateGroupProfile db user g p' (msg, _) <- sendGroupMessage user g' ms (XGrpInfo p') let cd = CDGroupSnd g' @@ -2016,6 +2020,10 @@ processChatCommand = \case toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat g') ci) createGroupFeatureChangedItems user cd CISndGroupFeature g g' pure $ CRGroupUpdated user g g' Nothing + checkValidName :: GroupName -> m () + checkValidName displayName = do + let validName = T.pack $ mkValidName $ T.unpack displayName + when (displayName /= validName) $ throwChatError CEInvalidDisplayName {displayName, validName} assertUserGroupRole :: GroupInfo -> GroupMemberRole -> m () assertUserGroupRole g@GroupInfo {membership} requiredRole = do when (membership.memberRole < requiredRole) $ throwChatError $ CEGroupUserRole g requiredRole @@ -5245,8 +5253,7 @@ getCreateActiveUser st testView = do where loop = do displayName <- getContactName - fullName <- T.pack <$> getWithPrompt "full name (optional)" - withTransaction st (\db -> runExceptT $ createUserRecord db (AgentUserId 1) Profile {displayName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing} True) >>= \case + withTransaction st (\db -> runExceptT $ createUserRecord db (AgentUserId 1) Profile {displayName, fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} True) >>= \case Left SEDuplicateName -> do putStrLn "chosen display name is already used by another profile on this device, choose another one" loop @@ -5276,10 +5283,13 @@ getCreateActiveUser st testView = do T.unpack $ localDisplayName <> if T.null fullName || localDisplayName == fullName then "" else " (" <> fullName <> ")" getContactName :: IO ContactName getContactName = do - displayName <- getWithPrompt "display name (no spaces)" - if null displayName || isJust (find (== ' ') displayName) - then putStrLn "display name has space(s), choose another one" >> getContactName - else pure $ T.pack displayName + displayName <- getWithPrompt "display name" + let validName = mkValidName displayName + if + | null displayName -> putStrLn "display name can't be empty" >> getContactName + | null validName -> putStrLn "display name is invalid, please choose another" >> getContactName + | displayName /= validName -> putStrLn ("display name is invalid, you could use this one: " <> validName) >> getContactName + | otherwise -> pure $ T.pack displayName getWithPrompt :: String -> IO String getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine @@ -5610,7 +5620,13 @@ chatCommandP = mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString msgContentP = "text " *> mcTextP <|> "json " *> jsonP ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal - displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' ')) + displayName = safeDecodeUtf8 <$> (quoted "'\"" <|> takeNameTill isSpace) + where + takeNameTill p = + A.peekChar' >>= \c -> + if refChar c then A.takeTill p else fail "invalid first character in display name" + quoted cs = A.choice [A.char c *> takeNameTill (== c) <* A.char c | c <- cs] + refChar c = c > ' ' && c /= '#' && c /= '@' sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> msgTextP quotedMsg = safeDecodeUtf8 <$> (A.char '(' *> A.takeTill (== ')') <* A.char ')') <* optional A.space reactionP = MREmoji <$> (mrEmojiChar <$?> (toEmoji <$> A.anyChar)) @@ -5623,7 +5639,6 @@ chatCommandP = '*' -> head "❤️" '^' -> '🚀' c -> c - refChar c = c > ' ' && c /= '#' && c /= '@' liveMessageP = " live=" *> onOffP <|> pure False sendMessageTTLP = " ttl=" *> ((Just <$> A.decimal) <|> ("default" $> Nothing)) <|> pure Nothing receiptSettings = do @@ -5718,3 +5733,16 @@ timeItToView s action = do let diff = diffToMilliseconds $ diffUTCTime t2 t1 toView $ CRTimedAction s diff pure a + +mkValidName :: String -> String +mkValidName = reverse . dropWhile isSpace . fst . foldl' addChar ("", '\NUL') + where + addChar (r, prev) c = if notProhibited && validChar then (c' : r, c') else (r, prev) + where + c' = if isSpace c then ' ' else c + validChar + | prev == '\NUL' || isSpace prev = validFirstChar + | isPunctuation prev = validFirstChar || isSpace c + | otherwise = validFirstChar || isSpace c || isMark c || isPunctuation c + validFirstChar = isLetter c || isNumber c || isSymbol c + notProhibited = c `notElem` ("@#'\"`" :: String) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 15c06cba94..61840b8e84 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -882,6 +882,7 @@ data ChatErrorType | CEEmptyUserPassword {userId :: UserId} | CEUserAlreadyHidden {userId :: UserId} | CEUserNotHidden {userId :: UserId} + | CEInvalidDisplayName {displayName :: Text, validName :: Text} | CEChatNotStarted | CEChatNotStopped | CEChatStoreChanged diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 700548bb12..783e38ef3f 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -65,6 +65,8 @@ foreign export ccall "chat_parse_server" cChatParseServer :: CString -> IO CJSON foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CString -> IO CString +foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString + foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString @@ -124,6 +126,10 @@ cChatPasswordHash cPwd cSalt = do salt <- B.packCString cSalt newCStringFromBS $ chatPasswordHash pwd salt +-- This function supports utf8 strings +cChatValidName :: CString -> IO CString +cChatValidName cName = newCString . mkValidName =<< peekCString cName + mobileChatOpts :: String -> String -> ChatOpts mobileChatOpts dbFilePrefix dbKey = ChatOpts diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 01bdfba95a..5eb9df3ad2 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -15,7 +15,7 @@ import Data.Aeson (ToJSON) import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB -import Data.Char (toUpper) +import Data.Char (isSpace, toUpper) import Data.Function (on) import Data.Int (Int64) import Data.List (groupBy, intercalate, intersperse, partition, sortOn) @@ -224,7 +224,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRLeftMember u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " left the group"] CRGroupEmpty u g -> ttyUser u [ttyFullGroup g <> ": group is empty"] CRGroupRemoved u g -> ttyUser u [ttyFullGroup g <> ": you are no longer a member or group deleted"] - CRGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"] + CRGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> viewGroupName g) <> " to delete the local copy of the group"] CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m CRGroupProfile u g -> ttyUser u $ viewGroupProfile g CRGroupDescription u g -> ttyUser u $ viewGroupDescription g @@ -674,10 +674,7 @@ viewContactNotFound cName suspectedMember = ["no contact " <> ttyContact cName <> useMessageMember] where useMessageMember = case suspectedMember of - Just (g, m) -> do - let GroupInfo {localDisplayName = gName} = g - GroupMember {localDisplayName = mName} = m - ", use " <> highlight' ("@#" <> T.unpack gName <> " " <> T.unpack mName <> " ") + Just (g, m) -> ", use " <> highlight ("@#" <> viewGroupName g <> " " <> viewMemberName m <> " ") _ -> "" viewChatCleared :: AChatInfo -> [StyledString] @@ -732,14 +729,14 @@ groupLink_ intro g cReq mRole = (plain . strEncode) cReq, "", "Anybody can connect to you and join group as " <> showRole mRole <> " with: " <> highlight' "/c ", - "to show it again: " <> highlight ("/show link #" <> groupName' g), - "to delete it: " <> highlight ("/delete link #" <> groupName' g) <> " (joined members will remain connected to you)" + "to show it again: " <> highlight ("/show link #" <> viewGroupName g), + "to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)" ] viewGroupLinkDeleted :: GroupInfo -> [StyledString] viewGroupLinkDeleted g = [ "Group link is deleted - joined members will remain connected.", - "To create a new group link use " <> highlight ("/create link #" <> groupName' g) + "To create a new group link use " <> highlight ("/create link #" <> viewGroupName g) ] viewSentInvitation :: Maybe Profile -> Bool -> [StyledString] @@ -756,20 +753,20 @@ viewSentInvitation incognitoProfile testView = viewReceivedContactRequest :: ContactName -> Profile -> [StyledString] viewReceivedContactRequest c Profile {fullName} = [ ttyFullName c fullName <> " wants to connect to you!", - "to accept: " <> highlight ("/ac " <> c), - "to reject: " <> highlight ("/rc " <> c) <> " (the sender will NOT be notified)" + "to accept: " <> highlight ("/ac " <> viewName c), + "to reject: " <> highlight ("/rc " <> viewName c) <> " (the sender will NOT be notified)" ] viewGroupCreated :: GroupInfo -> [StyledString] -viewGroupCreated g@GroupInfo {localDisplayName = n} = +viewGroupCreated g = [ "group " <> ttyFullGroup g <> " is created", - "to add members use " <> highlight ("/a " <> n <> " ") <> " or " <> highlight ("/create link #" <> n) + "to add members use " <> highlight ("/a " <> viewGroupName g <> " ") <> " or " <> highlight ("/create link #" <> viewGroupName g) ] viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString] -viewCannotResendInvitation GroupInfo {localDisplayName = gn} c = - [ ttyContact c <> " is already invited to group " <> ttyGroup gn, - "to re-send invitation: " <> highlight ("/rm " <> gn <> " " <> c) <> ", " <> highlight ("/a " <> gn <> " " <> c) +viewCannotResendInvitation g c = + [ ttyContact c <> " is already invited to group " <> ttyGroup' g, + "to re-send invitation: " <> highlight ("/rm " <> viewGroupName g <> " " <> c) <> ", " <> highlight ("/a " <> viewGroupName g <> " " <> viewName c) ] viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString] @@ -790,11 +787,11 @@ viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [Style viewReceivedGroupInvitation g c role = ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role) : case incognitoMembershipProfile g of - Just mp -> ["use " <> highlight ("/j " <> groupName' g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)] - Nothing -> ["use " <> highlight ("/j " <> groupName' g) <> " to accept"] + Just mp -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)] + Nothing -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to accept"] groupPreserved :: GroupInfo -> [StyledString] -groupPreserved g = ["use " <> highlight ("/d #" <> groupName' g) <> " to delete the group"] +groupPreserved g = ["use " <> highlight ("/d #" <> viewGroupName g) <> " to delete the group"] connectedMember :: GroupMember -> StyledString connectedMember m = case memberCategory m of @@ -845,7 +842,7 @@ viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filt _ -> "" viewContactConnected :: Contact -> Maybe Profile -> Bool -> [StyledString] -viewContactConnected ct@Contact {localDisplayName} userIncognitoProfile testView = +viewContactConnected ct userIncognitoProfile testView = case userIncognitoProfile of Just profile -> if testView @@ -854,7 +851,7 @@ viewContactConnected ct@Contact {localDisplayName} userIncognitoProfile testView where message = [ ttyFullContact ct <> ": contact is connected, your incognito profile for this contact is " <> incognitoProfile' profile, - "use " <> highlight ("/i " <> localDisplayName) <> " to print out this incognito profile again" + "use " <> highlight ("/i " <> viewContactName ct) <> " to print out this incognito profile again" ] Nothing -> [ttyFullContact ct <> ": contact is connected"] @@ -865,10 +862,10 @@ viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs where ldn_ :: GroupInfo -> Text ldn_ g = T.toLower g.localDisplayName - groupSS (g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership, chatSettings}, GroupSummary {currentMembers}) = + groupSS (g@GroupInfo {membership, chatSettings}, GroupSummary {currentMembers}) = case memberStatus membership of GSMemInvited -> groupInvitation' g - s -> membershipIncognito g <> ttyGroup ldn <> optFullName ldn fullName <> viewMemberStatus s + s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s where viewMemberStatus = \case GSMemRemoved -> delete "you are removed" @@ -876,18 +873,18 @@ viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs GSMemGroupDeleted -> delete "group deleted" _ | enableNtfs chatSettings -> " (" <> memberCount <> ")" - | otherwise -> " (" <> memberCount <> ", muted, you can " <> highlight ("/unmute #" <> ldn) <> ")" - delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> ldn) <> ")" + | otherwise -> " (" <> memberCount <> ", muted, you can " <> highlight ("/unmute #" <> viewGroupName g) <> ")" + delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> viewGroupName g) <> ")" memberCount = sShow currentMembers <> " member" <> if currentMembers == 1 then "" else "s" groupInvitation' :: GroupInfo -> StyledString groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}} = - highlight ("#" <> ldn) + highlight ("#" <> viewName ldn) <> optFullName ldn fullName <> " - you are invited (" - <> highlight ("/j " <> ldn) + <> highlight ("/j " <> viewName ldn) <> joinText - <> highlight ("/d #" <> ldn) + <> highlight ("/d #" <> viewName ldn) <> " to delete invitation)" where joinText = case incognitoMembershipProfile g of @@ -895,21 +892,21 @@ groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfil Nothing -> " to join, " viewContactsMerged :: Contact -> Contact -> [StyledString] -viewContactsMerged _into@Contact {localDisplayName = c1} _merged@Contact {localDisplayName = c2} = - [ "contact " <> ttyContact c2 <> " is merged into " <> ttyContact c1, - "use " <> ttyToContact c1 <> highlight' "" <> " to send messages" +viewContactsMerged c1 c2 = + [ "contact " <> ttyContact' c2 <> " is merged into " <> ttyContact' c1, + "use " <> ttyToContact' c1 <> highlight' "" <> " to send messages" ] viewUserProfile :: Profile -> [StyledString] viewUserProfile Profile {displayName, fullName} = [ "user profile: " <> ttyFullName displayName fullName, - "use " <> highlight' "/p []" <> " to change it", + "use " <> highlight' "/p " <> " to change it", "(the updated profile will be sent to all your contacts)" ] viewUserPrivacy :: User -> User -> [StyledString] viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', showNtfs, viewPwdHash} = - [ (if userId == userId' then "current " else "") <> "user " <> plain n' <> ":", + [ plain $ (if userId == userId' then "current " else "") <> "user " <> viewName n' <> ":", "messages are " <> if showNtfs then "shown" else "hidden (use /tail to view)", "profile is " <> if isJust viewPwdHash then "hidden" else "visible" ] @@ -1055,18 +1052,18 @@ viewGroupMemberSwitch g m (SwitchProgress qd phase _) = case qd of QDSnd -> [ttyGroup' g <> ": " <> ttyMember m <> " " <> viewSwitchPhase phase <> " for you"] viewContactRatchetSync :: Contact -> RatchetSyncProgress -> [StyledString] -viewContactRatchetSync ct@Contact {localDisplayName = c} RatchetSyncProgress {ratchetSyncStatus = rss} = +viewContactRatchetSync ct RatchetSyncProgress {ratchetSyncStatus = rss} = [ttyContact' ct <> ": " <> (plain . ratchetSyncStatusToText) rss] <> help where - help = ["use " <> highlight ("/sync " <> c) <> " to synchronize" | rss `elem` [RSAllowed, RSRequired]] + help = ["use " <> highlight ("/sync " <> viewContactName ct) <> " to synchronize" | rss `elem` [RSAllowed, RSRequired]] viewGroupMemberRatchetSync :: GroupInfo -> GroupMember -> RatchetSyncProgress -> [StyledString] -viewGroupMemberRatchetSync g m@GroupMember {localDisplayName = n} RatchetSyncProgress {ratchetSyncStatus = rss} = +viewGroupMemberRatchetSync g m RatchetSyncProgress {ratchetSyncStatus = rss} = [ttyGroup' g <> " " <> ttyMember m <> ": " <> (plain . ratchetSyncStatusToText) rss] <> help where - help = ["use " <> highlight ("/sync #" <> groupName' g <> " " <> n) <> " to synchronize" | rss `elem` [RSAllowed, RSRequired]] + help = ["use " <> highlight ("/sync #" <> viewGroupName g <> " " <> viewMemberName m) <> " to synchronize" | rss `elem` [RSAllowed, RSRequired]] viewContactVerificationReset :: Contact -> [StyledString] viewContactVerificationReset ct = @@ -1077,10 +1074,10 @@ viewGroupMemberVerificationReset g m = [ttyGroup' g <> " " <> ttyMember m <> ": security code changed"] viewContactCode :: Contact -> Text -> Bool -> [StyledString] -viewContactCode ct@Contact {localDisplayName = c} = viewSecurityCode (ttyContact' ct) ("/verify " <> c <> " ") +viewContactCode ct = viewSecurityCode (ttyContact' ct) ("/verify " <> viewContactName ct <> " ") viewGroupMemberCode :: GroupInfo -> GroupMember -> Text -> Bool -> [StyledString] -viewGroupMemberCode g m@GroupMember {localDisplayName = n} = viewSecurityCode (ttyGroup' g <> " " <> ttyMember m) ("/verify #" <> groupName' g <> " " <> n <> " ") +viewGroupMemberCode g m = viewSecurityCode (ttyGroup' g <> " " <> ttyMember m) ("/verify #" <> viewGroupName g <> " " <> viewMemberName m <> " ") viewSecurityCode :: StyledString -> Text -> Text -> Bool -> [StyledString] viewSecurityCode name cmd code testView @@ -1206,9 +1203,9 @@ bold' :: String -> StyledString bold' = styled Bold viewContactAliasUpdated :: Contact -> [StyledString] -viewContactAliasUpdated Contact {localDisplayName = n, profile = LocalProfile {localAlias}} - | localAlias == "" = ["contact " <> ttyContact n <> " alias removed"] - | otherwise = ["contact " <> ttyContact n <> " alias updated: " <> plain localAlias] +viewContactAliasUpdated ct@Contact {profile = LocalProfile {localAlias}} + | localAlias == "" = ["contact " <> ttyContact' ct <> " alias removed"] + | otherwise = ["contact " <> ttyContact' ct <> " alias updated: " <> plain localAlias] viewConnectionAliasUpdated :: PendingContactConnection -> [StyledString] viewConnectionAliasUpdated PendingContactConnection {pccConnId, localAlias} @@ -1385,10 +1382,10 @@ savingFile' testView (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, f savingFile' _ _ = ["saving file"] -- shouldn't happen receivingFile_' :: StyledString -> AChatItem -> [StyledString] -receivingFile_' status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) = - [status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact c] -receivingFile_' status (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupRcv GroupMember {localDisplayName = m}}) = - [status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact m] +receivingFile_' status (AChatItem _ _ (DirectChat c) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) = + [status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact' c] +receivingFile_' status (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupRcv m}) = + [status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyMember m] receivingFile_' status _ = [status <> " receiving file"] -- shouldn't happen receivingFile_ :: StyledString -> RcvFileTransfer -> [StyledString] @@ -1556,6 +1553,9 @@ viewChatError logLevel = \case CEEmptyUserPassword _ -> ["user password is required"] CEUserAlreadyHidden _ -> ["user is already hidden"] CEUserNotHidden _ -> ["user is not hidden"] + CEInvalidDisplayName {displayName, validName} -> map plain $ + ["invalid display name: " <> viewName displayName] + <> ["you could use this one: " <> viewName validName | not (T.null validName)] CEChatNotStarted -> ["error: chat not started"] CEChatNotStopped -> ["error: chat not stopped"] CEChatStoreChanged -> ["error: chat store changed, please restart chat"] @@ -1568,8 +1568,8 @@ viewChatError logLevel = \case ] CEContactNotFound cName m_ -> viewContactNotFound cName m_ CEContactNotReady c -> [ttyContact' c <> ": not ready"] + CEContactDisabled ct -> [ttyContact' ct <> ": disabled, to enable: " <> highlight ("/enable " <> viewContactName ct) <> ", to delete: " <> highlight ("/d " <> viewContactName ct)] CEContactNotActive c -> [ttyContact' c <> ": not active"] - CEContactDisabled Contact {localDisplayName = c} -> [ttyContact c <> ": disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)] CEConnectionDisabled Connection {connId, connType} -> [plain $ "connection " <> textEncode connType <> " (" <> tshow connId <> ") is disabled" | logLevel <= CLLWarning] CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"] CEGroupDuplicateMemberId -> ["cannot add member - duplicate member ID"] @@ -1581,7 +1581,7 @@ viewChatError logLevel = \case CEContactIncognitoCantInvite -> ["you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"] CEGroupIncognitoCantInvite -> ["you've connected to this group using an incognito profile - prohibited to invite contacts"] CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"] - CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> groupName' g)] + CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> viewGroupName g)] CEGroupMemberNotActive -> ["your group connection is not active yet, try later"] CEGroupMemberUserRemoved -> ["you are no longer a member of the group"] CEGroupMemberNotFound -> ["group doesn't have this member"] @@ -1641,8 +1641,8 @@ viewChatError logLevel = \case SEFileIdNotFoundBySharedMsgId _ -> [] -- recipient tried to accept cancelled file SEConnectionNotFound agentConnId -> ["event connection not found, agent ID: " <> sShow agentConnId | logLevel <= CLLWarning] -- mutes delete group error SEChatItemNotFoundByText text -> ["message not found by text: " <> plain text] - SEDuplicateGroupLink g -> ["you already have link for this group, to show: " <> highlight ("/show link #" <> groupName' g)] - SEGroupLinkNotFound g -> ["no group link, to create: " <> highlight ("/create link #" <> groupName' g)] + SEDuplicateGroupLink g -> ["you already have link for this group, to show: " <> highlight ("/show link #" <> viewGroupName g)] + SEGroupLinkNotFound g -> ["no group link, to create: " <> highlight ("/create link #" <> viewGroupName g)] e -> ["chat db error: " <> sShow e] ChatErrorDatabase err -> case err of DBErrorEncrypted -> ["error: chat database is already encrypted"] @@ -1687,8 +1687,8 @@ viewChatError logLevel = \case viewConnectionEntityDisabled :: ConnectionEntity -> [StyledString] viewConnectionEntityDisabled entity = case entity of - RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)] - RcvGroupMsgConnection _ GroupInfo {localDisplayName = g} GroupMember {localDisplayName = m} -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable #" <> g <> " " <> m)] + RcvDirectMsgConnection _ (Just c) -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable " <> viewContactName c) <> ", to delete: " <> highlight ("/d " <> viewContactName c)] + RcvGroupMsgConnection _ g m -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable #" <> viewGroupName g <> " " <> viewMemberName m)] _ -> ["[" <> entityLabel <> "] connection is disabled"] where entityLabel = connEntityLabel entity @@ -1703,7 +1703,7 @@ connEntityLabel = \case UserContactConnection _ UserContact {} -> "contact address" ttyContact :: ContactName -> StyledString -ttyContact = styled $ colored Green +ttyContact = styled (colored Green) . viewName ttyContact' :: Contact -> StyledString ttyContact' Contact {localDisplayName = c} = ttyContact c @@ -1723,37 +1723,46 @@ ttyFullName :: ContactName -> Text -> StyledString ttyFullName c fullName = ttyContact c <> optFullName c fullName ttyToContact :: ContactName -> StyledString -ttyToContact c = ttyTo $ "@" <> c <> " " +ttyToContact c = ttyTo $ "@" <> viewName c <> " " ttyToContact' :: Contact -> StyledString ttyToContact' ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyToContact c ttyToContactEdited' :: Contact -> StyledString -ttyToContactEdited' ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyTo ("@" <> c <> " [edited] ") +ttyToContactEdited' ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyTo ("@" <> viewName c <> " [edited] ") ttyQuotedContact :: Contact -> StyledString -ttyQuotedContact Contact {localDisplayName = c} = ttyFrom $ c <> ">" +ttyQuotedContact Contact {localDisplayName = c} = ttyFrom $ viewName c <> ">" ttyQuotedMember :: Maybe GroupMember -> StyledString -ttyQuotedMember (Just GroupMember {localDisplayName = c}) = "> " <> ttyFrom c +ttyQuotedMember (Just GroupMember {localDisplayName = c}) = "> " <> ttyFrom (viewName c) ttyQuotedMember _ = "> " <> ttyFrom "?" ttyFromContact :: Contact -> StyledString -ttyFromContact ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyFrom (c <> "> ") +ttyFromContact ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyFrom (viewName c <> "> ") ttyFromContactEdited :: Contact -> StyledString -ttyFromContactEdited ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyFrom (c <> "> [edited] ") +ttyFromContactEdited ct@Contact {localDisplayName = c} = ctIncognito ct <> ttyFrom (viewName c <> "> [edited] ") ttyFromContactDeleted :: Contact -> Maybe Text -> StyledString ttyFromContactDeleted ct@Contact {localDisplayName = c} deletedText_ = - ctIncognito ct <> ttyFrom (c <> "> " <> maybe "" (\t -> "[" <> t <> "] ") deletedText_) + ctIncognito ct <> ttyFrom (viewName c <> "> " <> maybe "" (\t -> "[" <> t <> "] ") deletedText_) ttyGroup :: GroupName -> StyledString -ttyGroup g = styled (colored Blue) $ "#" <> g +ttyGroup g = styled (colored Blue) $ "#" <> viewName g ttyGroup' :: GroupInfo -> StyledString ttyGroup' = ttyGroup . groupName' +viewContactName :: Contact -> Text +viewContactName = viewName . localDisplayName' + +viewGroupName :: GroupInfo -> Text +viewGroupName = viewName . groupName' + +viewMemberName :: GroupMember -> Text +viewMemberName GroupMember {localDisplayName = n} = viewName n + ttyGroups :: [GroupName] -> StyledString ttyGroups [] = "" ttyGroups [g] = ttyGroup g @@ -1774,8 +1783,7 @@ ttyFromGroupDeleted g m deletedText_ = membershipIncognito g <> ttyFrom (fromGroup_ g m <> maybe "" (\t -> "[" <> t <> "] ") deletedText_) fromGroup_ :: GroupInfo -> GroupMember -> Text -fromGroup_ GroupInfo {localDisplayName = g} GroupMember {localDisplayName = m} = - "#" <> g <> " " <> m <> "> " +fromGroup_ g m = "#" <> viewGroupName g <> " " <> viewMemberName m <> "> " ttyFrom :: Text -> StyledString ttyFrom = styled $ colored Yellow @@ -1784,12 +1792,13 @@ ttyTo :: Text -> StyledString ttyTo = styled $ colored Cyan ttyToGroup :: GroupInfo -> StyledString -ttyToGroup g@GroupInfo {localDisplayName = n} = - membershipIncognito g <> ttyTo ("#" <> n <> " ") +ttyToGroup g = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " ") ttyToGroupEdited :: GroupInfo -> StyledString -ttyToGroupEdited g@GroupInfo {localDisplayName = n} = - membershipIncognito g <> ttyTo ("#" <> n <> " [edited] ") +ttyToGroupEdited g = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " [edited] ") + +viewName :: Text -> Text +viewName s = if T.any isSpace s then "'" <> s <> "'" else s ttyFilePath :: FilePath -> StyledString ttyFilePath = plain diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 44af70a65e..da6cbd156f 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -8,7 +8,7 @@ import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) import qualified Data.Text as T -import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..)) +import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..), Profile (..)) import System.Directory (copyFile, createDirectoryIfMissing) import Test.Hspec @@ -17,6 +17,7 @@ chatProfileTests = do describe "user profiles" $ do it "update user profile and notify contacts" testUpdateProfile it "update user profile with image" testUpdateProfileImage + it "use multiword profile names" testMultiWordProfileNames describe "user contact link" $ do it "create and connect via contact link" testUserContactLink it "add contact link to profile" testProfileLink @@ -62,7 +63,7 @@ testUpdateProfile = createGroup3 "team" alice bob cath alice ##> "/p" alice <## "user profile: alice (Alice)" - alice <## "use /p [] to change it" + alice <## "use /p to change it" alice <## "(the updated profile will be sent to all your contacts)" alice ##> "/p alice" concurrentlyN_ @@ -117,6 +118,76 @@ testUpdateProfileImage = bob <## "use @alice2 to send messages" (bob FilePath -> IO () +testMultiWordProfileNames = + testChat3 aliceProfile' bobProfile' cathProfile' $ + \alice bob cath -> do + alice ##> "/c" + inv <- getInvitation alice + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + concurrently_ + (bob <## "'Alice Jones': contact is connected") + (alice <## "'Bob James': contact is connected") + alice #> "@'Bob James' hi" + bob <# "'Alice Jones'> hi" + alice ##> "/g 'Our Team'" + alice <## "group #'Our Team' is created" + alice <## "to add members use /a 'Our Team' or /create link #'Our Team'" + alice ##> "/a 'Our Team' 'Bob James' admin" + alice <## "invitation to join the group #'Our Team' sent to 'Bob James'" + bob <## "#'Our Team': 'Alice Jones' invites you to join the group as admin" + bob <## "use /j 'Our Team' to accept" + bob ##> "/j 'Our Team'" + bob <## "#'Our Team': you joined the group" + alice <## "#'Our Team': 'Bob James' joined the group" + bob ##> "/c" + inv' <- getInvitation bob + cath ##> ("/c " <> inv') + cath <## "confirmation sent!" + concurrently_ + (cath <## "'Bob James': contact is connected") + (bob <## "'Cath Johnson': contact is connected") + bob ##> "/a 'Our Team' 'Cath Johnson'" + bob <## "invitation to join the group #'Our Team' sent to 'Cath Johnson'" + cath <## "#'Our Team': 'Bob James' invites you to join the group as member" + cath <## "use /j 'Our Team' to accept" + cath ##> "/j 'Our Team'" + concurrentlyN_ + [ bob <## "#'Our Team': 'Cath Johnson' joined the group", + do + cath <## "#'Our Team': you joined the group" + cath <## "#'Our Team': member 'Alice Jones' is connected", + do + alice <## "#'Our Team': 'Bob James' added 'Cath Johnson' to the group (connecting...)" + alice <## "#'Our Team': new member 'Cath Johnson' is connected" + ] + bob #> "#'Our Team' hi" + alice <# "#'Our Team' 'Bob James'> hi" + cath <# "#'Our Team' 'Bob James'> hi" + alice `send` "@'Cath Johnson' hello" + alice <## "member #'Our Team' 'Cath Johnson' does not have direct connection, creating" + alice <## "contact for member #'Our Team' 'Cath Johnson' is created" + alice <## "sent invitation to connect directly to member #'Our Team' 'Cath Johnson'" + alice <# "@'Cath Johnson' hello" + cath <## "#'Our Team' 'Alice Jones' is creating direct contact 'Alice Jones' with you" + cath <# "'Alice Jones'> hello" + cath <## "'Alice Jones': contact is connected" + alice <## "'Cath Johnson': contact is connected" + cath ##> "/p 'Cath J'" + cath <## "user profile is changed to 'Cath J' (your 2 contacts are notified)" + alice <## "contact 'Cath Johnson' changed to 'Cath J'" + alice <## "use @'Cath J' to send messages" + bob <## "contact 'Cath Johnson' changed to 'Cath J'" + bob <## "use @'Cath J' to send messages" + alice #> "@'Cath J' hi" + cath <# "'Alice Jones'> hi" + where + aliceProfile' = baseProfile {displayName = "Alice Jones"} + bobProfile' = baseProfile {displayName = "Bob James"} + cathProfile' = baseProfile {displayName = "Cath Johnson"} + baseProfile = Profile {displayName = "", fullName = "", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} + testUserContactLink :: HasCallStack => FilePath -> IO () testUserContactLink = testChat3 aliceProfile bobProfile cathProfile $ diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index c120d661ff..6831cf3190 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -435,7 +435,7 @@ lastItemId cc = do showActiveUser :: HasCallStack => TestCC -> String -> Expectation showActiveUser cc name = do cc <## ("user profile: " <> name) - cc <## "use /p [] to change it" + cc <## "use /p to change it" cc <## "(the updated profile will be sent to all your contacts)" connectUsers :: HasCallStack => TestCC -> TestCC -> IO () diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 0b965250b4..69c2207ff6 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -61,6 +61,8 @@ mobileTests = do it "utf8 name 1" $ testFileEncryptionCApi "тест" it "utf8 name 2" $ testFileEncryptionCApi "👍" it "no exception on missing file" testMissingFileEncryptionCApi + describe "validate name" $ do + it "should convert invalid name to a valid name" testValidNameCApi noActiveUser :: LB.ByteString #if defined(darwin_HOST_OS) && defined(swiftJSON) @@ -266,6 +268,14 @@ testMissingFileEncryptionCApi tmp = do err' <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath' err' `shouldContain` toPath +testValidNameCApi :: FilePath -> IO () +testValidNameCApi _ = do + let goodName = "Джон Доу 👍" + cName1 <- cChatValidName =<< newCString goodName + peekCString cName1 `shouldReturn` goodName + cName2 <- cChatValidName =<< newCString " @'Джон' Доу 👍 " + peekCString cName2 `shouldReturn` goodName + jDecode :: FromJSON a => String -> IO (Maybe a) jDecode = pure . J.decode . LB.pack diff --git a/tests/Test.hs b/tests/Test.hs index 455d5459c7..cf60a70138 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -12,6 +12,7 @@ import SchemaDump import Test.Hspec import UnliftIO.Temporary (withTempDirectory) import ViewTests +import ValidNames import WebRTCTests main :: IO () @@ -23,6 +24,7 @@ main = do describe "SimpleX chat view" viewTests describe "SimpleX chat protocol" protocolTests describe "WebRTC encryption" webRTCTests + describe "Valid names" validNameTests around testBracket $ do describe "Mobile API Tests" mobileTests describe "SimpleX chat client" chatTests diff --git a/tests/ValidNames.hs b/tests/ValidNames.hs new file mode 100644 index 0000000000..40cda01431 --- /dev/null +++ b/tests/ValidNames.hs @@ -0,0 +1,27 @@ +module ValidNames where + +import Simplex.Chat +import Test.Hspec + +validNameTests :: Spec +validNameTests = describe "valid chat names" $ do + it "should keep valid and fix invalid names" testMkValidName + +testMkValidName :: IO () +testMkValidName = do + mkValidName "alice" `shouldBe` "alice" + mkValidName "алиса" `shouldBe` "алиса" + mkValidName "John Doe" `shouldBe` "John Doe" + mkValidName "J.Doe" `shouldBe` "J.Doe" + mkValidName "J. Doe" `shouldBe` "J. Doe" + mkValidName "J..Doe" `shouldBe` "J.Doe" + mkValidName "J ..Doe" `shouldBe` "J Doe" + mkValidName "J . . Doe" `shouldBe` "J Doe" + mkValidName "@alice" `shouldBe` "alice" + mkValidName "#alice" `shouldBe` "alice" + mkValidName " alice" `shouldBe` "alice" + mkValidName "alice " `shouldBe` "alice" + mkValidName "John Doe" `shouldBe` "John Doe" + mkValidName "'John Doe'" `shouldBe` "John Doe" + mkValidName "\"John Doe\"" `shouldBe` "John Doe" + mkValidName "`John Doe`" `shouldBe` "John Doe" From cc95fa6b305c7967b4c0b7a2ac0fac9310fb2ead Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 3 Oct 2023 20:46:17 +0800 Subject: [PATCH 13/17] desktop: paste files/images to attach to message (#3165) * desktop: paste files/images to attach to message * Windows * copy files inside the app * change * encrypted files support --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../platform/PlatformTextField.android.kt | 1 + .../views/chat/item/ChatItemView.android.kt | 6 +++ .../common/platform/PlatformTextField.kt | 3 ++ .../chat/simplex/common/views/TerminalView.kt | 1 + .../simplex/common/views/chat/ChatView.kt | 12 +----- .../simplex/common/views/chat/ComposeView.kt | 12 ++++++ .../simplex/common/views/chat/SendMsgView.kt | 8 +++- .../common/views/chat/item/ChatItemView.kt | 4 +- .../platform/PlatformTextField.desktop.kt | 39 ++++++++++++++++++- .../views/chat/item/ChatItemView.desktop.kt | 30 ++++++++++++-- 10 files changed, 97 insertions(+), 19 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt index 10faa1a82b..1bc9658496 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -50,6 +50,7 @@ actual fun PlatformTextField( userIsObserver: Boolean, onMessageChange: (String) -> Unit, onUpArrow: () -> Unit, + onFilesPasted: (List) -> Unit, onDone: () -> Unit, ) { val cs = composeState.value diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt index 8bb70c4a09..3aa4a92619 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt @@ -5,6 +5,8 @@ import android.os.Build import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatItem @@ -41,3 +43,7 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL showMenu.value = false }) } + +actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) { + clipboard.setText(AnnotatedString(cItem.content.text)) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt index 95b6a73ca4..fa99d0f93c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.text.TextStyle import chat.simplex.common.views.chat.ComposeState +import java.io.File +import java.net.URI @Composable expect fun PlatformTextField( @@ -14,5 +16,6 @@ expect fun PlatformTextField( userIsObserver: Boolean, onMessageChange: (String) -> Unit, onUpArrow: () -> Unit, + onFilesPasted: (List) -> Unit, onDone: () -> Unit, ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index c4f2a2cbde..a471b5645e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -97,6 +97,7 @@ fun TerminalLayout( updateLiveMessage = null, editPrevMessage = {}, onMessageChange = ::onMessageChange, + onFilesPasted = {}, textStyle = textStyle ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index ee9e109a7b..4afcdacbd8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -458,17 +458,7 @@ fun ChatLayout( .fillMaxWidth() .desktopOnExternalDrag( enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value, - onFiles = { paths -> - val uris = paths.map { URI.create(it) } - val groups = uris.groupBy { isImage(it) } - val images = groups[true] ?: emptyList() - val files = groups[false] ?: emptyList() - if (images.isNotEmpty()) { - CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(images, null) } - } else if (files.isNotEmpty()) { - composeState.processPickedFile(uris.first(), null) - } - }, + onFiles = { paths -> composeState.onFilesAttached(paths.map { URI.create(it) }) }, onImage = { val tmpFile = File.createTempFile("image", ".bmp", tmpDir) tmpFile.deleteOnExit() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index c6e6ca7b7a..972bc6621f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -159,6 +159,17 @@ expect fun AttachmentSelection( processPickedMedia: (List, String?) -> Unit ) +fun MutableState.onFilesAttached(uris: List) { + val groups = uris.groupBy { isImage(it) } + val images = groups[true] ?: emptyList() + val files = groups[false] ?: emptyList() + if (images.isNotEmpty()) { + CoroutineScope(Dispatchers.IO).launch { processPickedMedia(images, null) } + } else if (files.isNotEmpty()) { + processPickedFile(uris.first(), null) + } +} + fun MutableState.processPickedFile(uri: URI?, text: String?) { if (uri != null) { val fileSize = getFileSize(uri) @@ -816,6 +827,7 @@ fun ComposeView( chatModel.removeLiveDummy() }, editPrevMessage = ::editPrevMessage, + onFilesPasted = { composeState.onFilesAttached(it) }, onMessageChange = ::onMessageChange, textStyle = textStyle ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 2d696b7781..28882e6b73 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -29,6 +29,8 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.* +import java.io.File +import java.net.URI @Composable fun SendMsgView( @@ -52,6 +54,7 @@ fun SendMsgView( updateLiveMessage: (suspend () -> Unit)? = null, cancelLiveMessage: (() -> Unit)? = null, editPrevMessage: () -> Unit, + onFilesPasted: (List) -> Unit, onMessageChange: (String) -> Unit, textStyle: MutableState ) { @@ -79,7 +82,7 @@ fun SendMsgView( val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) val showDeleteTextButton = rememberSaveable { mutableStateOf(false) } - PlatformTextField(composeState, sendMsgEnabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) { + PlatformTextField(composeState, sendMsgEnabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) { if (!cs.inProgress) { sendMessage(null) } @@ -612,6 +615,7 @@ fun PreviewSendMsgView() { sendMessage = {}, editPrevMessage = {}, onMessageChange = { _ -> }, + onFilesPasted = {}, textStyle = textStyle ) } @@ -645,6 +649,7 @@ fun PreviewSendMsgViewEditing() { sendMessage = {}, editPrevMessage = {}, onMessageChange = { _ -> }, + onFilesPasted = {}, textStyle = textStyle ) } @@ -678,6 +683,7 @@ fun PreviewSendMsgViewInProgress() { sendMessage = {}, editPrevMessage = {}, onMessageChange = { _ -> }, + onFilesPasted = {}, textStyle = textStyle ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index b5236e2491..dd07a3fc19 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -201,7 +201,7 @@ fun ChatItemView( showMenu.value = false }) ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { - clipboard.setText(AnnotatedString(cItem.content.text)) + copyItemToClipboard(cItem, clipboard) showMenu.value = false }) if ((cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) && getLoadedFilePath(cItem.file) != null) { @@ -561,6 +561,8 @@ private fun showMsgDeliveryErrorAlert(description: String) { ) } +expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) + @Preview @Composable fun PreviewChatItemView() { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index 3b7ba84863..c677edc066 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.* @@ -27,6 +26,9 @@ import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.delay +import java.io.File +import java.net.URI +import kotlin.io.path.* import kotlin.math.min import kotlin.text.substring @@ -39,6 +41,7 @@ actual fun PlatformTextField( userIsObserver: Boolean, onMessageChange: (String) -> Unit, onUpArrow: () -> Unit, + onFilesPasted: (List) -> Unit, onDone: () -> Unit, ) { val cs = composeState.value @@ -63,10 +66,20 @@ actual fun PlatformTextField( val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) } var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) } val textFieldValue = textFieldValueState.copy(text = cs.message) + val clipboard = LocalClipboardManager.current BasicTextField( value = textFieldValue, - onValueChange = { + onValueChange = onValueChange@ { if (!composeState.value.inProgress && !(composeState.value.preview is ComposePreview.VoicePreview && it.text != "")) { + val diff = textFieldValueState.selection.length + (it.text.length - textFieldValueState.text.length) + if (diff > 1 && it.text != textFieldValueState.text && it.selection.max - diff >= 0) { + val pasted = it.text.substring(it.selection.max - diff, it.selection.max) + val files = parseToFiles(AnnotatedString(pasted)) + if (files.isNotEmpty()) { + onFilesPasted(files) + return@onValueChange + } + } textFieldValueState = it onMessageChange(it.text) } @@ -98,6 +111,12 @@ actual fun PlatformTextField( } else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.isEmpty()) { onUpArrow() true + } else if (it.key == Key.V && + it.type == KeyEventType.KeyDown && + ((it.isCtrlPressed && !desktopPlatform.isMac()) || (it.isMetaPressed && desktopPlatform.isMac())) && + parseToFiles(clipboard.getText()).isNotEmpty()) { + onFilesPasted(parseToFiles(clipboard.getText())) + true } else false }, @@ -142,3 +161,19 @@ private fun ComposeOverlay(textId: StringResource, textStyle: MutableState { + text ?: return emptyList() + val files = ArrayList() + text.lines().forEach { + try { + val uri = File(it.removePrefix("\"").removeSuffix("\"")).toURI() + val path = uri.toPath() + if (!path.exists() || !path.isAbsolute || path.isDirectory()) return emptyList() + files.add(uri) + } catch (e: Exception) { + return emptyList() + } + } + return files +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt index c1d9eeec52..9df5bd0a12 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt @@ -7,17 +7,19 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.foundation.layout.padding +import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.* -import chat.simplex.common.model.ChatItem -import chat.simplex.common.model.MsgContent -import chat.simplex.common.platform.FileChooserLauncher -import chat.simplex.common.platform.desktopPlatform +import chat.simplex.common.model.* +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.EmojiFont 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 +import java.io.File +import java.util.* @Composable actual fun ReactionIcon(text: String, fontSize: TextUnit) { @@ -39,3 +41,23 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL showMenu.value = false }) } + +actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) { + val fileSource = getLoadedFileSource(cItem.file) + if (fileSource != null) { + val filePath: String = if (fileSource.cryptoArgs != null) { + val tmpFile = File(tmpDir, fileSource.filePath) + tmpFile.deleteOnExit() + decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, tmpFile.absolutePath) + tmpFile.absolutePath + } else { + getAppFilePath(fileSource.filePath) + } + when { + desktopPlatform.isWindows() -> clipboard.setText(AnnotatedString("\"${File(filePath).absolutePath}\"")) + else -> clipboard.setText(AnnotatedString(filePath)) + } + } else { + clipboard.setText(AnnotatedString(cItem.content.text)) + } +} From 91fc238ddc95769ebcb6f32e63e08bb9bd6ada54 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 5 Oct 2023 00:06:23 +0800 Subject: [PATCH 14/17] desktop: libs refactoring (#3169) * desktop: libs refactoring * mac fix * windows fix * .gitignore * unused lines * desktop (windows): adapting Windows build to new libs * removed unused code --------- Co-authored-by: avently --- apps/multiplatform/.gitignore | 1 - .../src/commonMain/cpp/desktop/CMakeLists.txt | 7 +- .../simplex/common/platform/Files.desktop.kt | 2 - .../common/platform/Platform.desktop.kt | 12 +-- apps/multiplatform/desktop/build.gradle.kts | 91 ++++--------------- .../kotlin/chat/simplex/desktop/Main.kt | 42 ++------- scripts/desktop/build-lib-linux.sh | 24 ++++- scripts/desktop/build-lib-mac.sh | 28 ++++-- scripts/desktop/build-lib-windows.sh | 15 ++- scripts/desktop/download-lib-windows.sh | 13 ++- scripts/desktop/make-appimage-linux.sh | 9 +- scripts/desktop/prepare-vlc-linux.sh | 2 +- scripts/desktop/prepare-vlc-mac.sh | 2 +- scripts/desktop/prepare-vlc-windows.sh | 2 +- 14 files changed, 104 insertions(+), 146 deletions(-) diff --git a/apps/multiplatform/.gitignore b/apps/multiplatform/.gitignore index 81d296183d..f30061200c 100644 --- a/apps/multiplatform/.gitignore +++ b/apps/multiplatform/.gitignore @@ -11,7 +11,6 @@ local.properties common/src/commonMain/cpp/android/libs/ common/src/commonMain/cpp/desktop/libs/ -desktop/src/jvmMain/resources/libs/ android/build android/release common/build diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt index eb4794dd6a..059e5af426 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt @@ -54,12 +54,11 @@ add_library( # Sets the name of the library. simplex-api.c) add_library( simplex SHARED IMPORTED ) -# Lib has different name because of version, find it -FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex*.${OS_LIB_EXT}) - if(WIN32) + FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex*.${OS_LIB_EXT}) set_target_properties( simplex PROPERTIES IMPORTED_IMPLIB ${SIMPLEXLIB}) else() + FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex-chat*.${OS_LIB_EXT}) set_target_properties( simplex PROPERTIES IMPORTED_LOCATION ${SIMPLEXLIB}) endif() @@ -72,7 +71,7 @@ if(NOT APPLE) else() # Without direct linking it can't find hs_init in linking step add_library( rts SHARED IMPORTED ) - FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/deps/libHSrts*_thr-*.${OS_LIB_EXT}) + FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libHSrts*_thr-*.${OS_LIB_EXT}) set_target_properties( rts PROPERTIES IMPORTED_LOCATION ${RTSLIB}) target_link_libraries(app-lib rts simplex) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt index 46124a44fa..9042a62830 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt @@ -21,8 +21,6 @@ actual val agentDatabaseFileName: String = "simplex_v1_agent.db" actual val databaseExportDir: File = tmpDir -val vlcDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex-vlc").also { it.deleteOnExit() } - actual fun desktopOpenDatabaseDir() { if (Desktop.isDesktopSupported()) { try { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt index cb4e3acdb4..9217551a8d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt @@ -8,12 +8,12 @@ private val unixConfigPath = (System.getenv("XDG_CONFIG_HOME") ?: "$home/.config private val unixDataPath = (System.getenv("XDG_DATA_HOME") ?: "$home/.local/share") + "/simplex" val desktopPlatform = detectDesktopPlatform() -enum class DesktopPlatform(val libPath: String, val libExtension: String, val configPath: String, val dataPath: String) { - LINUX_X86_64("/libs/linux-x86_64", "so", unixConfigPath, unixDataPath), - LINUX_AARCH64("/libs/aarch64", "so", unixConfigPath, unixDataPath), - WINDOWS_X86_64("/libs/windows-x86_64", "dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"), - MAC_X86_64("/libs/mac-x86_64", "dylib", unixConfigPath, unixDataPath), - MAC_AARCH64("/libs/mac-aarch64", "dylib", unixConfigPath, unixDataPath); +enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String) { + LINUX_X86_64("so", unixConfigPath, unixDataPath), + LINUX_AARCH64("so", unixConfigPath, unixDataPath), + WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"), + MAC_X86_64("dylib", unixConfigPath, unixDataPath), + MAC_AARCH64("dylib", unixConfigPath, unixDataPath); fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64 fun isWindows() = this == WINDOWS_X86_64 diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index 3da87850d5..a7dab78ee8 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -52,6 +52,7 @@ compose { } //includeAllModules = true outputBaseDir.set(project.file("../release")) + appResourcesRootDir.set(project.file("../build/links")) targetFormats( TargetFormat.Deb, TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe //, TargetFormat.AppImage // Gradle doesn't sync on Mac with it @@ -156,11 +157,10 @@ tasks.named("compileJava") { afterEvaluate { tasks.create("cmakeBuildAndCopy") { dependsOn("cmakeBuild") - val copyDetails = mutableMapOf>() doLast { copy { - from("${project(":desktop").buildDir}/cmake/main/linux-amd64", "$cppPath/desktop/libs/linux-x86_64", "$cppPath/desktop/libs/linux-x86_64/deps") - into("src/jvmMain/resources/libs/linux-x86_64") + from("${project(":desktop").buildDir}/cmake/main/linux-amd64") + into("$cppPath/desktop/libs/linux-x86_64") include("*.so*") eachFile { path = name @@ -169,16 +169,8 @@ afterEvaluate { duplicatesStrategy = DuplicatesStrategy.INCLUDE } copy { - val destinationDir = "src/jvmMain/resources/libs/linux-x86_64/vlc" - from("$cppPath/desktop/libs/linux-x86_64/deps/vlc") - into(destinationDir) - includeEmptyDirs = false - duplicatesStrategy = DuplicatesStrategy.INCLUDE - copyIfNeeded(destinationDir, copyDetails) - } - copy { - from("${project(":desktop").buildDir}/cmake/main/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64/deps") - into("src/jvmMain/resources/libs/linux-aarch64") + from("${project(":desktop").buildDir}/cmake/main/linux-aarch64") + into("$cppPath/desktop/libs/linux-aarch64") include("*.so*") eachFile { path = name @@ -187,16 +179,18 @@ afterEvaluate { duplicatesStrategy = DuplicatesStrategy.INCLUDE } copy { - val destinationDir = "src/jvmMain/resources/libs/linux-aarch64/vlc" - from("$cppPath/desktop/libs/linux-aarch64/deps/vlc") - into(destinationDir) + from("${project(":desktop").buildDir}/cmake/main/windows-amd64") + into("$cppPath/desktop/libs/windows-x86_64") + include("*.dll") + eachFile { + path = name + } includeEmptyDirs = false duplicatesStrategy = DuplicatesStrategy.INCLUDE - copyIfNeeded(destinationDir, copyDetails) } - copy { - from("${project(":desktop").buildDir}/cmake/main/windows-amd64", "$cppPath/desktop/libs/windows-x86_64", "$cppPath/desktop/libs/windows-x86_64/deps") - into("src/jvmMain/resources/libs/windows-x86_64") + copy { + from("${project(":desktop").buildDir}/cmake/main/windows-amd64") + into("../build/links/windows-x64") include("*.dll") eachFile { path = name @@ -205,16 +199,8 @@ afterEvaluate { duplicatesStrategy = DuplicatesStrategy.INCLUDE } copy { - val destinationDir = "src/jvmMain/resources/libs/windows-x86_64/vlc" - from("$cppPath/desktop/libs/windows-x86_64/deps/vlc") - into(destinationDir) - includeEmptyDirs = false - duplicatesStrategy = DuplicatesStrategy.INCLUDE - copyIfNeeded(destinationDir, copyDetails) - } - copy { - from("${project(":desktop").buildDir}/cmake/main/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64/deps") - into("src/jvmMain/resources/libs/mac-x86_64") + from("${project(":desktop").buildDir}/cmake/main/mac-x86_64") + into("$cppPath/desktop/libs/mac-x86_64") include("*.dylib") eachFile { path = name @@ -223,16 +209,8 @@ afterEvaluate { duplicatesStrategy = DuplicatesStrategy.INCLUDE } copy { - val destinationDir = "src/jvmMain/resources/libs/mac-x86_64/vlc" - from("$cppPath/desktop/libs/mac-x86_64/deps/vlc") - into(destinationDir) - includeEmptyDirs = false - duplicatesStrategy = DuplicatesStrategy.INCLUDE - copyIfNeeded(destinationDir, copyDetails) - } - copy { - from("${project(":desktop").buildDir}/cmake/main/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64/deps") - into("src/jvmMain/resources/libs/mac-aarch64") + from("${project(":desktop").buildDir}/cmake/main/mac-aarch64") + into("$cppPath/desktop/libs/mac-aarch64") include("*.dylib") eachFile { path = name @@ -240,39 +218,6 @@ afterEvaluate { includeEmptyDirs = false duplicatesStrategy = DuplicatesStrategy.INCLUDE } - copy { - val destinationDir = "src/jvmMain/resources/libs/mac-aarch64/vlc" - from("$cppPath/desktop/libs/mac-aarch64/deps/vlc") - into(destinationDir) - includeEmptyDirs = false - duplicatesStrategy = DuplicatesStrategy.INCLUDE - copyIfNeeded(destinationDir, copyDetails) - } - } - afterEvaluate { - doLast { - copyDetails.forEach { (destinationDir, details) -> - details.forEach { detail -> - val target = File(projectDir.absolutePath + File.separator + destinationDir + File.separator + detail.path) - if (target.exists()) { - target.setLastModified(detail.lastModified) - } - } - } - } } } } - -fun CopySpec.copyIfNeeded(destinationDir: String, into: MutableMap>) { - val details = arrayListOf() - eachFile { - val targetFile = File(destinationDir, path) - if (file.lastModified() == targetFile.lastModified() && file.length() == targetFile.length()) { - exclude() - } else { - details.add(this) - } - } - into[destinationDir] = details -} diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index a0be877329..ff92b08ff0 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -18,51 +18,29 @@ fun main() { @Suppress("UnsafeDynamicallyLoadedCode") private fun initHaskell() { - val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs") - copyResources(desktopPlatform.libPath, libsTmpDir.toPath()) - vlcDir.deleteRecursively() - Files.move(File(libsTmpDir, "vlc").toPath(), vlcDir.toPath(), StandardCopyOption.REPLACE_EXISTING) + val resourcesDir = File(System.getProperty("compose.application.resources.dir")) + val vlcDir = File(resourcesDir.absolutePath + File.separator + "vlc") if (desktopPlatform == DesktopPlatform.WINDOWS_X86_64) { - windowsLoadRequiredLibs(libsTmpDir) + windowsLoadRequiredLibs(resourcesDir, vlcDir) } else { - System.load(File(libsTmpDir, "libapp-lib.${desktopPlatform.libExtension}").absolutePath) + System.load(File(resourcesDir, "libapp-lib.${desktopPlatform.libExtension}").absolutePath) } // No picture without preloading it, only sound. However, with libs from AppImage it works without preloading //val libXcb = "libvlc_xcb_events.so.0.0.0" //System.load(File(File(vlcDir, "vlc"), libXcb).absolutePath) System.setProperty("jna.library.path", vlcDir.absolutePath) //discoverVlcLibs(File(File(vlcDir, "vlc"), "plugins").absolutePath) - - libsTmpDir.deleteRecursively() initHS() } -private fun copyResources(from: String, to: Path) { - val resource = Class.forName("chat.simplex.desktop.MainKt").getResource("")!!.toURI() - val fileSystem = FileSystems.newFileSystem(resource, emptyMap()) - val resPath = fileSystem.getPath(from) - Files.walkFileTree(resPath, object: SimpleFileVisitor() { - override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { - Files.createDirectories(to.resolve(resPath.relativize(dir).toString())) - return FileVisitResult.CONTINUE - } - override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { - val dest = to.resolve(resPath.relativize(file).toString()) - Files.copy(file, dest, StandardCopyOption.REPLACE_EXISTING) - // Setting the same time on file as the time set in script that generates VLC libs - if (dest.toString().contains("." + desktopPlatform.libExtension)) { - dest.setLastModifiedTime(FileTime.fromMillis(0)) - } - return FileVisitResult.CONTINUE - } - }) -} - -private fun windowsLoadRequiredLibs(libsTmpDir: File) { +private fun windowsLoadRequiredLibs(libsTmpDir: File, vlcDir: File) { val mainLibs = arrayOf( "libcrypto-3-x64.dll", + "mcfgthread-12.dll", + "libgcc_s_seh-1.dll", + "libstdc++-6.dll", "libffi-8.dll", - "libgmp-10.dll", + "libgmp-10.dll", "libsimplex.dll", "libapp-lib.dll" ) @@ -72,7 +50,7 @@ private fun windowsLoadRequiredLibs(libsTmpDir: File) { val vlcLibs = arrayOf( "libvlccore.dll", "libvlc.dll", - "axvlc.dll", + "axvlc.dll", "npvlc.dll" ) vlcLibs.forEach { diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index 2d9681fc6f..e0ee7e6698 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -1,9 +1,23 @@ #!/bin/bash +set -e + +function readlink() { + echo "$(cd "$(dirname "$1")"; pwd -P)" +} + OS=linux ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`} GHC_VERSION=9.6.2 +if [ "$ARCH" == "aarch64" ]; then + COMPOSE_ARCH=arm64 +else + COMPOSE_ARCH=x64 +fi + +root_dir="$(dirname "$(dirname "$(readlink "$0")")")" +cd $root_dir BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-* rm -rf $BUILD_DIR @@ -11,16 +25,20 @@ cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts - cd $BUILD_DIR/build #patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so #patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so -mkdir deps +mkdir deps 2> /dev/null || true ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/ cd - rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ scripts/desktop/prepare-vlc-linux.sh + +links_dir=apps/multiplatform/build/links +mkdir -p $links_dir +cd $links_dir +ln -sfT ../../common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ $OS-$COMPOSE_ARCH diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index 303e331549..3680a4a2aa 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -4,10 +4,13 @@ set -e OS=mac ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}" +COMPOSE_ARCH=$ARCH GHC_VERSION=9.6.2 if [ "$ARCH" == "arm64" ]; then ARCH=aarch64 +else + COMPOSE_ARCH=x64 fi LIB_EXT=dylib @@ -84,30 +87,29 @@ rm deps/`basename $LIB` cd - rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -LIBCRYPTO_PATH=$(otool -l deps/libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) -install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSdrct-*.$LIB_EXT -cp $LIBCRYPTO_PATH deps/libcrypto.1.1.$LIB_EXT -chmod 755 deps/libcrypto.1.1.$LIB_EXT -install_name_tool -id "libcrypto.1.1.$LIB_EXT" deps/libcrypto.1.1.$LIB_EXT -install_name_tool -id "libffi.8.$LIB_EXT" deps/libffi.$LIB_EXT +LIBCRYPTO_PATH=$(otool -l libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) +install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSdrct-*.$LIB_EXT +cp $LIBCRYPTO_PATH libcrypto.1.1.$LIB_EXT +chmod 755 libcrypto.1.1.$LIB_EXT +install_name_tool -id "libcrypto.1.1.$LIB_EXT" libcrypto.1.1.$LIB_EXT +install_name_tool -id "libffi.8.$LIB_EXT" libffi.$LIB_EXT LIBCRYPTO_PATH=$(otool -l $LIB | grep libcrypto | cut -d' ' -f11) if [ -n "$LIBCRYPTO_PATH" ]; then install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT $LIB fi -LIBCRYPTO_PATH=$(otool -l deps/libHSsmplxmq*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) +LIBCRYPTO_PATH=$(otool -l libHSsmplxmq*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) if [ -n "$LIBCRYPTO_PATH" ]; then - install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSsmplxmq*.$LIB_EXT + install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSsmplxmq*.$LIB_EXT fi for lib in $(find . -type f -name "*.$LIB_EXT"); do @@ -126,3 +128,9 @@ fi cd - scripts/desktop/prepare-vlc-mac.sh + +links_dir=apps/multiplatform/build/links +mkdir -p $links_dir +cd $links_dir +rm macos-$COMPOSE_ARCH 2>/dev/null | true +ln -sf ../../common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ macos-$COMPOSE_ARCH diff --git a/scripts/desktop/build-lib-windows.sh b/scripts/desktop/build-lib-windows.sh index 658324baa8..ef39ef8683 100755 --- a/scripts/desktop/build-lib-windows.sh +++ b/scripts/desktop/build-lib-windows.sh @@ -8,15 +8,26 @@ function readlink() { root_dir="$(dirname "$(dirname "$(readlink "$0")")")" OS=windows -ARCH=`uname -a | rev | cut -d' ' -f2 | rev` +ARCH="x86_64" JOB_REPO=${1:-$SIMPLEX_CI_REPO_URL} +if [ "$ARCH" == "aarch64" ]; then + COMPOSE_ARCH=arm64 +else + COMPOSE_ARCH=x64 +fi + cd $root_dir rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ scripts/desktop/download-lib-windows.sh $JOB_REPO scripts/desktop/prepare-vlc-windows.sh + +links_dir=apps/multiplatform/build/links +mkdir -p $links_dir +cd $links_dir +rm -rf $OS-$COMPOSE_ARCH +ln -sfT ../../common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ $OS-$COMPOSE_ARCH diff --git a/scripts/desktop/download-lib-windows.sh b/scripts/desktop/download-lib-windows.sh index 14439274c0..945bd7b5e7 100644 --- a/scripts/desktop/download-lib-windows.sh +++ b/scripts/desktop/download-lib-windows.sh @@ -7,7 +7,7 @@ function readlink() { } if [ -z "${1}" ]; then - echo "Job repo is unset. Provide it via first argument like: $(readlink "$0")/download-lib-windows.sh https://something.com/job/something/{windows,windows-8107}" + echo "Job repo is unset. Provide it via first argument like: $(readlink "$0")/download-lib-windows.sh https://something.com/job/something/{master,stable}" exit 1 fi @@ -16,12 +16,15 @@ arch=x86_64 root_dir="$(dirname "$(dirname "$(readlink "$0")")")" output_dir="$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-$arch/" -mkdir -p "$output_dir"/deps 2> /dev/null +mkdir -p "$output_dir" 2> /dev/null curl --location -o libsimplex.zip $job_repo/$arch-linux.$arch-windows:lib:simplex-chat/latest/download/1 && \ $WINDIR\\System32\\tar.exe -xf libsimplex.zip && \ mv libsimplex.dll "$output_dir" && \ -mv libcrypto*.dll "$output_dir/deps" && \ -mv libffi*.dll "$output_dir/deps" && \ -mv libgmp*.dll "$output_dir/deps" && \ +mv libcrypto*.dll "$output_dir" && \ +mv libffi*.dll "$output_dir" && \ +mv libgmp*.dll "$output_dir" && \ +mv mcfgthread*.dll "$output_dir" && \ +mv libgcc_s_seh*.dll "$output_dir" && \ +mv libstdc++*.dll "$output_dir" && \ rm libsimplex.zip diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index 35e62481db..9cd6f525f5 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -11,13 +11,12 @@ multiplatform_dir=$root_dir/apps/multiplatform release_app_dir=$root_dir/apps/multiplatform/release/main/app cd $multiplatform_dir -libcrypto_path=$(ldd common/src/commonMain/cpp/desktop/libs/*/deps/libHSdirect-sqlcipher-*.so | grep libcrypto | cut -d'=' -f 2 | cut -d ' ' -f 2) -trap "rm common/src/commonMain/cpp/desktop/libs/*/deps/`basename $libcrypto_path` 2> /dev/null || true" EXIT -cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/*/deps +libcrypto_path=$(ldd common/src/commonMain/cpp/desktop/libs/*/libHSdirect-sqlcipher-*.so | grep libcrypto | cut -d'=' -f 2 | cut -d ' ' -f 2) +trap "rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path` 2> /dev/null || true" EXIT +cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/* ./gradlew createDistributable -rm common/src/commonMain/cpp/desktop/libs/*/deps/`basename $libcrypto_path` -rm desktop/src/jvmMain/resources/libs/*/`basename $libcrypto_path` +rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path` rm -rf $release_app_dir/AppDir 2>/dev/null mkdir -p $release_app_dir/AppDir/usr diff --git a/scripts/desktop/prepare-vlc-linux.sh b/scripts/desktop/prepare-vlc-linux.sh index e1cfa7e9fc..a76486150c 100755 --- a/scripts/desktop/prepare-vlc-linux.sh +++ b/scripts/desktop/prepare-vlc-linux.sh @@ -6,7 +6,7 @@ function readlink() { echo "$(cd "$(dirname "$1")"; pwd -P)" } root_dir="$(dirname "$(dirname "$(readlink "$0")")")" -vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linux-x86_64/deps/vlc +vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linux-x86_64/vlc mkdir $vlc_dir || exit 0 diff --git a/scripts/desktop/prepare-vlc-mac.sh b/scripts/desktop/prepare-vlc-mac.sh index 69644bcc16..25ec1365fc 100755 --- a/scripts/desktop/prepare-vlc-mac.sh +++ b/scripts/desktop/prepare-vlc-mac.sh @@ -16,7 +16,7 @@ function readlink() { } root_dir="$(dirname "$(dirname "$(readlink "$0")")")" -vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/mac-$ARCH/deps/vlc +vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/mac-$ARCH/vlc #rm -rf $vlc_dir mkdir -p $vlc_dir/vlc || exit 0 diff --git a/scripts/desktop/prepare-vlc-windows.sh b/scripts/desktop/prepare-vlc-windows.sh index bdb492344a..680fa7b803 100644 --- a/scripts/desktop/prepare-vlc-windows.sh +++ b/scripts/desktop/prepare-vlc-windows.sh @@ -6,7 +6,7 @@ function readlink() { echo "$(cd "$(dirname "$1")"; pwd -P)" } root_dir="$(dirname "$(dirname "$(readlink "$0")")")" -vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-x86_64/deps/vlc +vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-x86_64/vlc rm -rf $vlc_dir mkdir -p $vlc_dir/vlc || exit 0 From 0d8558a6d04688e6ed000ec73b8c1a54ba17508b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 4 Oct 2023 17:45:39 +0100 Subject: [PATCH 15/17] ios: profile names (remove full name) (#3168) * ios: profile names (remove full name) * create/update groups * focus display name --- .../Views/Chat/Group/GroupProfileView.swift | 67 +++- .../Shared/Views/NewChat/AddGroupView.swift | 41 +-- .../Views/Onboarding/CreateProfile.swift | 341 +++++++++++------- .../Views/Onboarding/OnboardingView.swift | 2 +- .../Views/UserSettings/SettingsView.swift | 4 +- .../Views/UserSettings/UserProfile.swift | 51 ++- apps/ios/SimpleXChat/SimpleX.h | 1 + 7 files changed, 315 insertions(+), 192 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 729556e739..6ff3af66a5 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -9,6 +9,18 @@ import SwiftUI import SimpleXChat +enum GroupProfileAlert: Identifiable { + case saveError(err: String) + case invalidName(validName: String) + + var id: String { + switch self { + case let .saveError(err): return "saveError \(err)" + case let .invalidName(validName): return "invalidName \(validName)" + } + } +} + struct GroupProfileView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss: DismissAction @@ -18,8 +30,7 @@ struct GroupProfileView: View { @State private var showImagePicker = false @State private var showTakePhoto = false @State private var chosenImage: UIImage? = nil - @State private var showSaveErrorAlert = false - @State private var saveGroupError: String? = nil + @State private var alert: GroupProfileAlert? @FocusState private var focusDisplayName var body: some View { @@ -47,20 +58,29 @@ struct GroupProfileView: View { .frame(maxWidth: .infinity, alignment: .center) VStack(alignment: .leading) { - ZStack(alignment: .leading) { - if !validDisplayName(groupProfile.displayName) { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .padding(.bottom, 10) + ZStack(alignment: .topLeading) { + if !validNewProfileName() { + Button { + alert = .invalidName(validName: mkValidName(groupProfile.displayName)) + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } else { + Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } profileNameTextEdit("Group display name", $groupProfile.displayName) .focused($focusDisplayName) } - profileNameTextEdit("Group full name (optional)", $groupProfile.fullName) + .padding(.bottom) + let fullName = groupInfo.groupProfile.fullName + if fullName != "" && fullName != groupProfile.displayName { + profileNameTextEdit("Group full name (optional)", $groupProfile.fullName) + .padding(.bottom) + } HStack(spacing: 20) { Button("Cancel") { dismiss() } Button("Save group profile") { saveProfile() } - .disabled(groupProfile.displayName == "" || !validDisplayName(groupProfile.displayName)) + .disabled(groupProfile.displayName == "" || !validNewProfileName()) } } .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) @@ -99,27 +119,35 @@ struct GroupProfileView: View { focusDisplayName = true } } - .alert(isPresented: $showSaveErrorAlert) { - Alert( - title: Text("Error saving group profile"), - message: Text("\(saveGroupError ?? "Unexpected error")") - ) + .alert(item: $alert) { a in + switch a { + case let .saveError(err): + return Alert( + title: Text("Error saving group profile"), + message: Text(err) + ) + case let .invalidName(name): + return createInvalidNameAlert(name, $groupProfile.displayName) + } } .contentShape(Rectangle()) .onTapGesture { hideKeyboard() } } + private func validNewProfileName() -> Bool { + groupProfile.displayName == groupInfo.groupProfile.displayName + || validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces)) + } + func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding) -> some View { TextField(label, text: name) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .padding(.bottom) - .padding(.leading, 28) + .padding(.leading, 32) } func saveProfile() { Task { do { + groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces) let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile) await MainActor.run { groupInfo = gInfo @@ -128,8 +156,7 @@ struct GroupProfileView: View { } } catch let error { let err = responseError(error) - saveGroupError = err - showSaveErrorAlert = true + alert = .saveError(err: err) logger.error("GroupProfile apiUpdateGroup error: \(err)") } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 8df37bb560..5cea52cc8b 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -16,11 +16,11 @@ struct AddGroupView: View { @State private var groupInfo: GroupInfo? @State private var profile = GroupProfile(displayName: "", fullName: "") @FocusState private var focusDisplayName - @FocusState private var focusFullName @State private var showChooseSource = false @State private var showImagePicker = false @State private var showTakePhoto = false @State private var chosenImage: UIImage? = nil + @State private var showInvalidNameAlert = false var body: some View { if let chat = chat, let groupInfo = groupInfo { @@ -76,26 +76,24 @@ struct AddGroupView: View { .padding(.bottom, 4) ZStack(alignment: .topLeading) { - if !validDisplayName(profile.displayName) { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .padding(.top, 4) + let name = profile.displayName.trimmingCharacters(in: .whitespaces) + if name != mkValidName(name) { + Button { + showInvalidNameAlert = true + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } else { + Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } - textField("Group display name", text: $profile.displayName) + textField("Enter group name…", text: $profile.displayName) .focused($focusDisplayName) - .submitLabel(.next) + .submitLabel(.go) .onSubmit { - if canCreateProfile() { focusFullName = true } - else { focusDisplayName = true } + if canCreateProfile() { createGroup() } } } - textField("Group full name (optional)", text: $profile.fullName) - .focused($focusFullName) - .submitLabel(.go) - .onSubmit { - if canCreateProfile() { createGroup() } - else { focusFullName = true } - } + .padding(.bottom) Spacer() @@ -133,6 +131,9 @@ struct AddGroupView: View { didSelectItem in showImagePicker = false } } + .alert(isPresented: $showInvalidNameAlert) { + createInvalidNameAlert(mkValidName(profile.displayName), $profile.displayName) + } .onChange(of: chosenImage) { image in if let image = image { profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) @@ -146,15 +147,13 @@ struct AddGroupView: View { func textField(_ placeholder: LocalizedStringKey, text: Binding) -> some View { TextField(placeholder, text: text) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .padding(.leading, 28) - .padding(.bottom) + .padding(.leading, 32) } func createGroup() { hideKeyboard() do { + profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) let gInfo = try apiNewGroup(profile) Task { let groupMembers = await apiListMembers(gInfo.groupId) @@ -180,7 +179,7 @@ struct AddGroupView: View { } func canCreateProfile() -> Bool { - profile.displayName != "" && validDisplayName(profile.displayName) + profile.displayName != "" && validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces)) } } diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index d05ac44588..f5db37dacf 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -9,175 +9,244 @@ import SwiftUI import SimpleXChat +enum UserProfileAlert: Identifiable { + case duplicateUserError + case createUserError(error: LocalizedStringKey) + case invalidNameError(validName: String) + + var id: String { + switch self { + case .duplicateUserError: return "duplicateUserError" + case .createUserError: return "createUserError" + case let .invalidNameError(validName): return "invalidNameError \(validName)" + } + } +} + struct CreateProfile: View { + @Environment(\.dismiss) var dismiss + @State private var displayName: String = "" + @FocusState private var focusDisplayName + @State private var alert: UserProfileAlert? + + var body: some View { + List { + Section { + TextField("Enter your name…", text: $displayName) + .focused($focusDisplayName) + Button { + createProfile(displayName, showAlert: { alert = $0 }, dismiss: dismiss) + } label: { + Label("Create profile", systemImage: "checkmark") + } + .disabled(!canCreateProfile(displayName)) + } header: { + HStack { + Text("Your profile") + let name = displayName.trimmingCharacters(in: .whitespaces) + let validName = mkValidName(name) + if name != validName { + Spacer() + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + .onTapGesture { + alert = .invalidNameError(validName: validName) + } + } + } + .frame(height: 20) + } footer: { + VStack(alignment: .leading, spacing: 8) { + Text("Your profile, contacts and delivered messages are stored on your device.") + Text("The profile is only shared with your contacts.") + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .navigationTitle("Create your profile") + .alert(item: $alert) { a in userProfileAlert(a, $displayName) } + .onAppear() { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + focusDisplayName = true + } + } + .keyboardPadding() + } +} + +struct CreateFirstProfile: View { @EnvironmentObject var m: ChatModel @Environment(\.dismiss) var dismiss @State private var displayName: String = "" - @State private var fullName: String = "" @FocusState private var focusDisplayName - @FocusState private var focusFullName - @State private var alert: CreateProfileAlert? - - private enum CreateProfileAlert: Identifiable { - case duplicateUserError - case createUserError(error: LocalizedStringKey) - - var id: String { - switch self { - case .duplicateUserError: return "duplicateUserError" - case .createUserError: return "createUserError" - } - } - } var body: some View { VStack(alignment: .leading) { - Text("Create your profile") - .font(.largeTitle) - .bold() - .padding(.bottom, 4) - .frame(maxWidth: .infinity) - Text("Your profile, contacts and delivered messages are stored on your device.") - .padding(.bottom, 4) - Text("The profile is only shared with your contacts.") - .padding(.bottom) + Group { + Text("Create your profile") + .font(.largeTitle) + .bold() + Text("Your profile, contacts and delivered messages are stored on your device.") + .foregroundColor(.secondary) + Text("The profile is only shared with your contacts.") + .foregroundColor(.secondary) + .padding(.bottom) + } + .padding(.bottom) + ZStack(alignment: .topLeading) { - if !validDisplayName(displayName) { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .padding(.top, 4) + let name = displayName.trimmingCharacters(in: .whitespaces) + let validName = mkValidName(name) + if name != validName { + Button { + showAlert(.invalidNameError(validName: validName)) + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } else { + Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } - textField("Display name", text: $displayName) + TextField("Enter your name…", text: $displayName) .focused($focusDisplayName) - .submitLabel(.next) - .onSubmit { - if canCreateProfile() { focusFullName = true } - else { focusDisplayName = true } - } + .padding(.leading, 32) } - textField("Full name (optional)", text: $fullName) - .focused($focusFullName) - .submitLabel(.go) - .onSubmit { - if canCreateProfile() { createProfile() } - else { focusFullName = true } - } - + .padding(.bottom) Spacer() - - HStack { - if m.users.isEmpty { - Button { - hideKeyboard() - withAnimation { - m.onboardingStage = .step1_SimpleXInfo - } - } label: { - HStack { - Image(systemName: "lessthan") - Text("About SimpleX") - } - } - } - - Spacer() - - HStack { - Button { - createProfile() - } label: { - Text("Create") - Image(systemName: "greaterthan") - } - .disabled(!canCreateProfile()) - } - } + onboardingButtons() } .onAppear() { focusDisplayName = true setLastVersionDefault() } - .alert(item: $alert) { a in - switch a { - case .duplicateUserError: return duplicateUserAlert - case let .createUserError(err): return creatUserErrorAlert(err) - } - } .padding() + .frame(maxWidth: .infinity, alignment: .leading) .keyboardPadding() } - func textField(_ placeholder: LocalizedStringKey, text: Binding) -> some View { - TextField(placeholder, text: text) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .padding(.leading, 28) - .padding(.bottom) - } - - func createProfile() { - hideKeyboard() - let profile = Profile( - displayName: displayName, - fullName: fullName - ) - do { - m.currentUser = try apiCreateActiveUser(profile) - if m.users.isEmpty { - try startChat() + func onboardingButtons() -> some View { + HStack { + Button { + hideKeyboard() withAnimation { - onboardingStageDefault.set(.step3_CreateSimpleXAddress) - m.onboardingStage = .step3_CreateSimpleXAddress + m.onboardingStage = .step1_SimpleXInfo } - } else { - onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete - dismiss() - m.users = try listUsers() - try getUserChatData() - } - } catch let error { - switch error as? ChatResponse { - case .chatCmdError(_, .errorStore(.duplicateName)), - .chatCmdError(_, .error(.userExists)): - if m.currentUser == nil { - AlertManager.shared.showAlert(duplicateUserAlert) - } else { - alert = .duplicateUserError - } - default: - let err: LocalizedStringKey = "Error: \(responseError(error))" - if m.currentUser == nil { - AlertManager.shared.showAlert(creatUserErrorAlert(err)) - } else { - alert = .createUserError(error: err) + } label: { + HStack { + Image(systemName: "lessthan") + Text("About SimpleX") } } - logger.error("Failed to create user or start chat: \(responseError(error))") + + Spacer() + + Button { + createProfile(displayName, showAlert: showAlert, dismiss: dismiss) + } label: { + HStack { + Text("Create") + Image(systemName: "greaterthan") + } + } + .disabled(!canCreateProfile(displayName)) } } - func canCreateProfile() -> Bool { - displayName != "" && validDisplayName(displayName) - } - - private var duplicateUserAlert: Alert { - Alert( - title: Text("Duplicate display name!"), - message: Text("You already have a chat profile with the same display name. Please choose another name.") - ) - } - - private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert { - Alert( - title: Text("Error creating profile!"), - message: Text(err) - ) + private func showAlert(_ alert: UserProfileAlert) { + AlertManager.shared.showAlert(userProfileAlert(alert, $displayName)) } } +private func createProfile(_ displayName: String, showAlert: (UserProfileAlert) -> Void, dismiss: DismissAction) { + hideKeyboard() + let profile = Profile( + displayName: displayName.trimmingCharacters(in: .whitespaces), + fullName: "" + ) + let m = ChatModel.shared + do { + m.currentUser = try apiCreateActiveUser(profile) + if m.users.isEmpty { + try startChat() + withAnimation { + onboardingStageDefault.set(.step3_CreateSimpleXAddress) + m.onboardingStage = .step3_CreateSimpleXAddress + } + } else { + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete + dismiss() + m.users = try listUsers() + try getUserChatData() + } + } catch let error { + switch error as? ChatResponse { + case .chatCmdError(_, .errorStore(.duplicateName)), + .chatCmdError(_, .error(.userExists)): + if m.currentUser == nil { + AlertManager.shared.showAlert(duplicateUserAlert) + } else { + showAlert(.duplicateUserError) + } + default: + let err: LocalizedStringKey = "Error: \(responseError(error))" + if m.currentUser == nil { + AlertManager.shared.showAlert(creatUserErrorAlert(err)) + } else { + showAlert(.createUserError(error: err)) + } + } + logger.error("Failed to create user or start chat: \(responseError(error))") + } +} + +private func canCreateProfile(_ displayName: String) -> Bool { + let name = displayName.trimmingCharacters(in: .whitespaces) + return name != "" && mkValidName(name) == name +} + +func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding) -> Alert { + switch alert { + case .duplicateUserError: return duplicateUserAlert + case let .createUserError(err): return creatUserErrorAlert(err) + case let .invalidNameError(name): return createInvalidNameAlert(name, displayName) + } +} + +private var duplicateUserAlert: Alert { + Alert( + title: Text("Duplicate display name!"), + message: Text("You already have a chat profile with the same display name. Please choose another name.") + ) +} + +private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert { + Alert( + title: Text("Error creating profile!"), + message: Text(err) + ) +} + +func createInvalidNameAlert(_ name: String, _ displayName: Binding) -> Alert { + name == "" + ? Alert(title: Text("Invalid name!")) + : Alert( + title: Text("Invalid name!"), + message: Text("Correct name to \(name)?"), + primaryButton: .default( + Text("Ok"), + action: { displayName.wrappedValue = name } + ), + secondaryButton: .cancel() + ) +} + func validDisplayName(_ name: String) -> Bool { - name.firstIndex(of: " ") == nil && name.first != "@" && name.first != "#" + mkValidName(name.trimmingCharacters(in: .whitespaces)) == name +} + +func mkValidName(_ s: String) -> String { + var c = s.cString(using: .utf8)! + return fromCString(chat_valid_name(&c)!) } struct CreateProfile_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index b0734be642..438491b5f1 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -14,7 +14,7 @@ struct OnboardingView: View { var body: some View { switch onboarding { case .step1_SimpleXInfo: SimpleXInfo(onboarding: true) - case .step2_CreateProfile: CreateProfile() + case .step2_CreateProfile: CreateFirstProfile() case .step3_CreateSimpleXAddress: CreateSimpleXAddress() case .step4_SetNotificationsMode: SetNotificationsMode() case .onboardingComplete: EmptyView() diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index d6681a51cd..f076a4eb7d 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -381,7 +381,9 @@ struct ProfilePreview: View { Text(profileOf.displayName) .fontWeight(.bold) .font(.title2) - Text(profileOf.fullName) + if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName { + Text(profileOf.fullName) + } } } } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index f38dc593a6..88be0b6a7f 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -17,6 +17,8 @@ struct UserProfile: View { @State private var showImagePicker = false @State private var showTakePhoto = false @State private var chosenImage: UIImage? = nil + @State private var alert: UserProfileAlert? + @FocusState private var focusDisplayName var body: some View { let user: User = chatModel.currentUser! @@ -47,18 +49,27 @@ struct UserProfile: View { VStack(alignment: .leading) { ZStack(alignment: .leading) { - if !validDisplayName(profile.displayName) { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .padding(.bottom, 10) + if !validNewProfileName(user) { + Button { + alert = .invalidNameError(validName: mkValidName(profile.displayName)) + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } else { + Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } - profileNameTextEdit("Display name", $profile.displayName) + profileNameTextEdit("Profile name", $profile.displayName) + .focused($focusDisplayName) + } + .padding(.bottom) + if showFullName(user) { + profileNameTextEdit("Full name (optional)", $profile.fullName) + .padding(.bottom) } - profileNameTextEdit("Full name (optional)", $profile.fullName) HStack(spacing: 20) { Button("Cancel") { editProfile = false } Button("Save (and notify contacts)") { saveProfile() } - .disabled(profile.displayName == "" || !validDisplayName(profile.displayName)) + .disabled(!canSaveProfile(user)) } } .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) @@ -74,11 +85,14 @@ struct UserProfile: View { .frame(maxWidth: .infinity, alignment: .center) VStack(alignment: .leading) { - profileNameView("Display name:", user.profile.displayName) - profileNameView("Full name:", user.profile.fullName) + profileNameView("Profile name:", user.profile.displayName) + if showFullName(user) { + profileNameView("Full name:", user.profile.fullName) + } Button("Edit") { profile = fromLocalProfile(user.profile) editProfile = true + focusDisplayName = true } } .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) @@ -117,14 +131,12 @@ struct UserProfile: View { profile.image = nil } } + .alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) } } func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding) -> some View { TextField(label, text: name) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .padding(.bottom) - .padding(.leading, 28) + .padding(.leading, 32) } func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View { @@ -141,9 +153,22 @@ struct UserProfile: View { showChooseSource = true } + private func validNewProfileName(_ user: User) -> Bool { + profile.displayName == user.profile.displayName || validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces)) + } + + private func showFullName(_ user: User) -> Bool { + user.profile.fullName != "" && user.profile.fullName != user.profile.displayName + } + + private func canSaveProfile(_ user: User) -> Bool { + profile.displayName != "" && validNewProfileName(user) + } + func saveProfile() { Task { do { + profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) if let (newProfile, _) = try await apiUpdateProfile(profile: profile) { DispatchQueue.main.async { chatModel.updateCurrentUser(newProfile) diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 67c2fa728c..9db3f06ae7 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -23,6 +23,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); extern char *chat_password_hash(char *pwd, char *salt); +extern char *chat_valid_name(char *name); extern char *chat_encrypt_media(char *key, char *frame, int len); extern char *chat_decrypt_media(char *key, char *frame, int len); From 34e1e44338e77f038821d4e6a86bc70df7c6ba22 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 6 Oct 2023 04:49:18 +0800 Subject: [PATCH 16/17] android, desktop: profile names (remove full name) (#3177) * desktop, android: profile names (remove full name) * rename back * disallow spaces only in names * ios: disallow spaces only in names * changes --- .../Views/Chat/Group/GroupProfileView.swift | 6 +- .../Shared/Views/NewChat/AddGroupView.swift | 3 +- .../Views/UserSettings/UserProfile.swift | 2 +- .../src/commonMain/cpp/android/simplex-api.c | 9 + .../src/commonMain/cpp/desktop/simplex-api.c | 11 +- .../kotlin/chat/simplex/common/App.kt | 3 +- .../chat/simplex/common/platform/Core.kt | 1 + .../chat/simplex/common/views/WelcomeView.kt | 276 ++++++++++++------ .../views/chat/group/GroupProfileView.kt | 46 +-- .../common/views/newchat/AddGroupView.kt | 31 +- .../common/views/onboarding/OnboardingView.kt | 40 --- .../common/views/usersettings/SettingsView.kt | 2 +- .../views/usersettings/UserProfileView.kt | 64 ++-- .../views/usersettings/UserProfilesView.kt | 3 +- .../commonMain/resources/MR/base/strings.xml | 11 +- 15 files changed, 288 insertions(+), 220 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 6ff3af66a5..7e123c389f 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -80,7 +80,7 @@ struct GroupProfileView: View { HStack(spacing: 20) { Button("Cancel") { dismiss() } Button("Save group profile") { saveProfile() } - .disabled(groupProfile.displayName == "" || !validNewProfileName()) + .disabled(!canUpdateProfile()) } } .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) @@ -134,6 +134,10 @@ struct GroupProfileView: View { .onTapGesture { hideKeyboard() } } + private func canUpdateProfile() -> Bool { + groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName() + } + private func validNewProfileName() -> Bool { groupProfile.displayName == groupInfo.groupProfile.displayName || validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces)) diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 5cea52cc8b..186a24e995 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -179,7 +179,8 @@ struct AddGroupView: View { } func canCreateProfile() -> Bool { - profile.displayName != "" && validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces)) + let name = profile.displayName.trimmingCharacters(in: .whitespaces) + return name != "" && validDisplayName(name) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 88be0b6a7f..b1a362a5a1 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -162,7 +162,7 @@ struct UserProfile: View { } private func canSaveProfile(_ user: User) -> Bool { - profile.displayName != "" && validNewProfileName(user) + profile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName(user) } func saveProfile() { diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index dbfee70257..351ed93c97 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -46,6 +46,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); +extern char *chat_valid_name(const char *name); extern char *chat_write_file(const char *path, char *ptr, int length); extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_encrypt_file(const char *from_path, const char *to_path); @@ -121,6 +122,14 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused return res; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz, jstring name) { + const char *_name = (*env)->GetStringUTFChars(env, name, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_valid_name(_name)); + (*env)->ReleaseStringUTFChars(env, name, _name); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) { const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index ddc5c92f93..f36c86c366 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -21,6 +21,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); +extern char *chat_valid_name(const char *name); extern char *chat_write_file(const char *path, char *ptr, int length); extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_encrypt_file(const char *from_path, const char *to_path); @@ -75,7 +76,7 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass cla jstring res = decode_to_utf8_string(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl)); (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); (*env)->ReleaseStringUTFChars(env, dbKey, _dbKey); - (*env)->ReleaseStringUTFChars(env, dbKey, _confirm); + (*env)->ReleaseStringUTFChars(env, confirm, _confirm); // Creating array of Object's (boxed values can be passed, eg. Long instead of long) jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL); @@ -133,6 +134,14 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass cl return res; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz, jstring name) { + const char *_name = encode_to_utf8_chars(env, name); + jstring res = decode_to_utf8_string(env, chat_valid_name(_name)); + (*env)->ReleaseStringUTFChars(env, name, _name); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) { const char *_path = encode_to_utf8_chars(env, path); diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index c08ad5f914..4531f88f96 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -17,6 +17,7 @@ import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.CreateFirstProfile import chat.simplex.common.views.helpers.SimpleButton import chat.simplex.common.views.SplashView import chat.simplex.common.views.call.ActiveCallView @@ -135,7 +136,7 @@ fun MainScreen() { ModalManager.fullscreen.showInView() } } - onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {} + onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel) onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 801a0270e2..2bed24b1f6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -20,6 +20,7 @@ external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatParseMarkdown(str: String): String external fun chatParseServer(str: String): String external fun chatPasswordHash(pwd: String, salt: String): String +external fun chatValidName(name: String): String external fun chatWriteFile(path: String, buffer: ByteBuffer): String external fun chatReadFile(path: String, key: String, nonce: String): Array external fun chatEncryptFile(fromPath: String, toPath: String): String diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 13ce16d0a7..504ecac89d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -1,10 +1,10 @@ package chat.simplex.common.views +import SectionTextFooter import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.material.MaterialTheme.colors import androidx.compose.runtime.* @@ -18,115 +18,160 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatModel import chat.simplex.common.model.Profile -import chat.simplex.common.platform.appPlatform -import chat.simplex.common.platform.navigationBarsWithImePadding +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.OnboardingStage -import chat.simplex.common.views.onboarding.ReadableText +import chat.simplex.common.views.onboarding.* +import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged - -fun isValidDisplayName(name: String) : Boolean { - return (name.firstOrNull { it.isWhitespace() }) == null && !name.startsWith("@") && !name.startsWith("#") -} +import kotlinx.coroutines.launch @Composable -fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) { - val displayName = rememberSaveable { mutableStateOf("") } - val fullName = rememberSaveable { mutableStateOf("") } - val focusRequester = remember { FocusRequester() } +fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + val keyboardState by getKeyboardState() + var savedKeyboardState by remember { mutableStateOf(keyboardState) } - Column( - modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) - ) { - /*CloseSheetBar(close = { - if (chatModel.users.isEmpty()) { - chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo - } else { - close() - } - })*/ - Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) - ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1) - ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1) - Spacer(Modifier.height(DEFAULT_PADDING)) - Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text( - stringResource(MR.strings.display_name), - fontSize = 16.sp - ) - if (!isValidDisplayName(displayName.value)) { - Text( - stringResource(MR.strings.no_spaces), - fontSize = 16.sp, - color = Color.Red - ) - } - } - ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) - Spacer(Modifier.height(DEFAULT_PADDING)) - Text( - stringResource(MR.strings.full_name_optional__prompt), - fontSize = 16.sp, - modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) - ) - ProfileNameField(fullName, "") - } - Spacer(Modifier.fillMaxHeight().weight(1f)) - Row { - if (chatModel.users.isEmpty()) { - SimpleButtonDecorated( - text = stringResource(MR.strings.about_simplex), - icon = painterResource(MR.images.ic_arrow_back_ios_new), - textDecoration = TextDecoration.None, - fontWeight = FontWeight.Medium - ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } - } - Spacer(Modifier.fillMaxWidth().weight(1f)) - val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) - val createModifier: Modifier - val createColor: Color - if (enabled) { - createModifier = Modifier.clickable { - if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { - createProfileInProfiles(chatModel, displayName.value, fullName.value, close) - } else { - createProfileOnboarding(chatModel, displayName.value, fullName.value, close) + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 20.dp) + ) { + val displayName = rememberSaveable { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + + Column( + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) + ) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) + Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + stringResource(MR.strings.display_name), + fontSize = 16.sp + ) + val name = displayName.value.trim() + val validName = mkValidName(name) + Spacer(Modifier.height(20.dp)) + if (name != validName) { + IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } + } + } + ProfileNameField(displayName, "", { it.trim() == mkValidName(it) }, focusRequester) + } + SettingsActionItem( + painterResource(MR.images.ic_check), + stringResource(MR.strings.create_another_profile_button), + disabled = !canCreateProfile(displayName.value), + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary, + click = { createProfileInProfiles(chatModel, displayName.value, close) }, + ) + SectionTextFooter(generalGetString(MR.strings.your_profile_is_stored_on_your_device)) + SectionTextFooter(generalGetString(MR.strings.profile_is_only_shared_with_your_contacts)) + + LaunchedEffect(Unit) { + delay(300) + focusRequester.requestFocus() + } + } + if (savedKeyboardState != keyboardState) { + LaunchedEffect(keyboardState) { + scope.launch { + savedKeyboardState = keyboardState + scrollState.animateScrollTo(scrollState.maxValue) } - }.padding(8.dp) - createColor = MaterialTheme.colors.primary - } else { - createModifier = Modifier.padding(8.dp) - createColor = MaterialTheme.colors.secondary - } - Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) { - Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium) - Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor) } } - } - - LaunchedEffect(Unit) { - delay(300) - focusRequester.requestFocus() } } } -fun createProfileInProfiles(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) { +@Composable +fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + val keyboardState by getKeyboardState() + var savedKeyboardState by remember { mutableStateOf(keyboardState) } + + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 20.dp) + ) { + val displayName = rememberSaveable { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + + Column( + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) + ) { + /*CloseSheetBar(close = { + if (chatModel.users.isEmpty()) { + chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo + } else { + close() + } + })*/ + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) + ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) + ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) + Spacer(Modifier.height(DEFAULT_PADDING)) + Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + stringResource(MR.strings.display_name), + fontSize = 16.sp + ) + val name = displayName.value.trim() + val validName = mkValidName(name) + Spacer(Modifier.height(20.dp)) + if (name != validName) { + IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } + } + } + ProfileNameField(displayName, "", { it.trim() == mkValidName(it) }, focusRequester) + } + Spacer(Modifier.fillMaxHeight().weight(1f)) + OnboardingButtons(displayName, close) + + LaunchedEffect(Unit) { + delay(300) + focusRequester.requestFocus() + } + } + LaunchedEffect(Unit) { + setLastVersionDefault(chatModel) + } + if (savedKeyboardState != keyboardState) { + LaunchedEffect(keyboardState) { + scope.launch { + savedKeyboardState = keyboardState + scrollState.animateScrollTo(scrollState.maxValue) + } + } + } + } + } +} + +fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) { withApi { val user = chatModel.controller.apiCreateActiveUser( - Profile(displayName, fullName, null) + Profile(displayName.trim(), "", null) ) ?: return@withApi chatModel.currentUser.value = user if (chatModel.users.isEmpty()) { @@ -142,10 +187,10 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, fullName: } } -fun createProfileOnboarding(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) { +fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) { withApi { chatModel.controller.apiCreateActiveUser( - Profile(displayName, fullName, null) + Profile(displayName.trim(), "", null) ) ?: return@withApi val onboardingStage = chatModel.controller.appPrefs.onboardingStage if (chatModel.users.isEmpty()) { @@ -163,6 +208,28 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, fullName: } } +@Composable +fun OnboardingButtons(displayName: MutableState, close: () -> Unit) { + Row { + SimpleButtonDecorated( + text = stringResource(MR.strings.about_simplex), + icon = painterResource(MR.images.ic_arrow_back_ios_new), + textDecoration = TextDecoration.None, + fontWeight = FontWeight.Medium + ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } + Spacer(Modifier.fillMaxWidth().weight(1f)) + val enabled = canCreateProfile(displayName.value) + val createModifier: Modifier = Modifier.clickable(enabled) { createProfileOnboarding(chatModel, displayName.value, close) }.padding(8.dp) + val createColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) { + Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium) + Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor) + } + } + } +} + @Composable fun ProfileNameField(name: MutableState, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) { var valid by rememberSaveable { mutableStateOf(true) } @@ -195,10 +262,6 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal onValueChange = { name.value = it }, modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester), textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground), - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - autoCorrect = false - ), singleLine = true, cursorBrush = SolidColor(MaterialTheme.colors.secondary) ) @@ -211,3 +274,28 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal } } } + +private fun canCreateProfile(displayName: String): Boolean { + val name = displayName.trim() + return name.isNotEmpty() && mkValidName(name) == name +} + +fun showInvalidNameAlert(name: String, displayName: MutableState) { + if (name.isEmpty()) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_name), + ) + } else { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.invalid_name), + text = generalGetString(MR.strings.correct_name_to).format(name), + onConfirm = { + displayName.value = name + } + ) + } +} + +fun isValidDisplayName(name: String) : Boolean = mkValidName(name.trim()) == name + +fun mkValidName(s: String): String = chatValidName(s) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index ac13dd483a..5376cb092b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -19,12 +19,12 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.ProfileNameField +import chat.simplex.common.views.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.net.URI @@ -65,13 +65,13 @@ fun GroupProfileLayout( fullName.value == groupProfile.fullName && groupProfile.image == profileImage.value val closeWithAlert = { - if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) { + if (dataUnchanged || !canUpdateProfile(displayName.value, groupProfile)) { close() } else { showUnsavedChangesAlert({ saveProfile( groupProfile.copy( - displayName = displayName.value, + displayName = displayName.value.trim(), fullName = fullName.value, image = profileImage.value ) @@ -125,32 +125,32 @@ fun GroupProfileLayout( stringResource(MR.strings.group_display_name_field), fontSize = 16.sp ) - if (!isValidDisplayName(displayName.value)) { + if (!isValidNewProfileName(displayName.value, groupProfile)) { Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - Text( - stringResource(MR.strings.no_spaces), - fontSize = 16.sp, - color = Color.Red - ) + IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } } } - ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) + ProfileNameField(displayName, "", { isValidNewProfileName(it, groupProfile) }, focusRequester) + if (groupProfile.fullName.isNotEmpty() && groupProfile.fullName != groupProfile.displayName) { + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.group_full_name_field), + fontSize = 16.sp, + modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) + ) + ProfileNameField(fullName) + } Spacer(Modifier.height(DEFAULT_PADDING)) - Text( - stringResource(MR.strings.group_full_name_field), - fontSize = 16.sp, - modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) - ) - ProfileNameField(fullName) - Spacer(Modifier.height(DEFAULT_PADDING)) - val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) + val enabled = !dataUnchanged && canUpdateProfile(displayName.value, groupProfile) if (enabled) { Text( stringResource(MR.strings.save_group_profile), modifier = Modifier.clickable { saveProfile( groupProfile.copy( - displayName = displayName.value, + displayName = displayName.value.trim(), fullName = fullName.value, image = profileImage.value ) @@ -178,6 +178,12 @@ fun GroupProfileLayout( } } +private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean = + displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, groupProfile) + +private fun isValidNewProfileName(displayName: String, groupProfile: GroupProfile): Boolean = + displayName == groupProfile.displayName || isValidDisplayName(displayName.trim()) + private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { AlertManager.shared.showAlertDialogStacked( title = generalGetString(MR.strings.save_preferences_question), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 3ab5ef9b2b..6ad919c27d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -19,15 +19,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.ProfileNameField import chat.simplex.common.views.chat.group.AddGroupMembersView import chat.simplex.common.views.chatlist.setGroupMembers import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.usersettings.DeleteImageButton import chat.simplex.common.views.usersettings.EditImageButton import chat.simplex.common.platform.* +import chat.simplex.common.views.* import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -60,7 +59,6 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) { val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() val displayName = rememberSaveable { mutableStateOf("") } - val fullName = rememberSaveable { mutableStateOf("") } val chosenImage = rememberSaveable { mutableStateOf(null) } val profileImage = rememberSaveable { mutableStateOf(null) } val focusRequester = remember { FocusRequester() } @@ -110,31 +108,22 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) { stringResource(MR.strings.group_display_name_field), fontSize = 16.sp ) - if (!isValidDisplayName(displayName.value)) { + if (!isValidDisplayName(displayName.value.trim())) { Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - Text( - stringResource(MR.strings.no_spaces), - fontSize = 16.sp, - color = Color.Red - ) + IconButton({ showInvalidNameAlert(mkValidName(displayName.value.trim()), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } } } - ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) - Spacer(Modifier.height(DEFAULT_PADDING)) - Text( - stringResource(MR.strings.group_full_name_field), - fontSize = 16.sp, - modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) - ) - ProfileNameField(fullName, "") + ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) Spacer(Modifier.height(8.dp)) - val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) + val enabled = canCreateProfile(displayName.value) if (enabled) { CreateGroupButton(MaterialTheme.colors.primary, Modifier .clickable { createGroup(GroupProfile( - displayName = displayName.value, - fullName = fullName.value, + displayName = displayName.value.trim(), + fullName = "", image = profileImage.value )) } @@ -167,6 +156,8 @@ fun CreateGroupButton(color: Color, modifier: Modifier) { } } +fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim()) + @Preview @Composable fun PreviewAddGroupLayout() { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt index 119ed8cd48..0a48bbd1eb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt @@ -1,16 +1,5 @@ package chat.simplex.common.views.onboarding -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.ProvideWindowInsets -import chat.simplex.common.views.CreateProfilePanel -import chat.simplex.common.platform.getKeyboardState -import kotlinx.coroutines.launch - enum class OnboardingStage { Step1_SimpleXInfo, Step2_CreateProfile, @@ -19,32 +8,3 @@ enum class OnboardingStage { Step4_SetNotificationsMode, OnboardingComplete } - -@Composable -fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { - val scope = rememberCoroutineScope() - val scrollState = rememberScrollState() - val keyboardState by getKeyboardState() - var savedKeyboardState by remember { mutableStateOf(keyboardState) } - - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(top = 20.dp) - ) { - CreateProfilePanel(chatModel, close) - LaunchedEffect(Unit) { - setLastVersionDefault(chatModel) - } - if (savedKeyboardState != keyboardState) { - LaunchedEffect(keyboardState) { - scope.launch { - savedKeyboardState = keyboardState - scrollState.animateScrollTo(scrollState.maxValue) - } - } - } - } - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index eebe7b3f49..d949f800bb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -364,7 +364,7 @@ fun AppVersionItem(showVersion: () -> Unit) { maxLines = 1, overflow = TextOverflow.Ellipsis ) - if (profileOf.fullName.isNotEmpty()) { + if (profileOf.fullName.isNotEmpty() && profileOf.fullName != profileOf.displayName) { Text( profileOf.fullName, Modifier.padding(vertical = 5.dp), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt index a7ae3a6751..38c9e58ad2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt @@ -17,14 +17,12 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.ProfileNameField import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.onboarding.ReadableText -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.Profile import chat.simplex.common.platform.* +import chat.simplex.common.views.* import chat.simplex.res.MR import kotlinx.coroutines.launch import java.net.URI @@ -39,7 +37,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { close, saveProfile = { displayName, fullName, image -> withApi { - val updated = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image)) + val updated = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName.trim(), fullName = fullName, image = image)) if (updated != null) { val (newProfile, _) = updated chatModel.updateCurrentUser(newProfile) @@ -89,7 +87,7 @@ fun UserProfileLayout( profile.image == profileImage.value val closeWithAlert = { - if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) { + if (dataUnchanged || !canSaveProfile(displayName.value, profile)) { close() } else { showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, profileImage.value) }, close) @@ -128,36 +126,27 @@ fun UserProfileLayout( stringResource(MR.strings.display_name__field), fontSize = 16.sp ) - if (!isValidDisplayName(displayName.value)) { - Spacer(Modifier.size(DEFAULT_PADDING_HALF)) - Text( - stringResource(MR.strings.no_spaces), - fontSize = 16.sp, - color = Color.Red - ) + if (!isValidNewProfileName(displayName.value, profile)) { + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } } } - ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) - Spacer(Modifier.height(DEFAULT_PADDING)) - Text( - stringResource(MR.strings.full_name__field), - fontSize = 16.sp, - modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) - ) - ProfileNameField(fullName) - - Spacer(Modifier.height(DEFAULT_PADDING)) - val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) - val saveModifier: Modifier - val saveColor: Color - if (enabled) { - saveModifier = Modifier - .clickable { saveProfile(displayName.value, fullName.value, profileImage.value) } - saveColor = MaterialTheme.colors.primary - } else { - saveModifier = Modifier - saveColor = MaterialTheme.colors.secondary + ProfileNameField(displayName, "", { isValidNewProfileName(it, profile) }, focusRequester) + if (showFullName(profile)) { + Spacer(Modifier.height(DEFAULT_PADDING)) + Text( + stringResource(MR.strings.full_name__field), + fontSize = 16.sp, + modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) + ) + ProfileNameField(fullName) } + Spacer(Modifier.height(DEFAULT_PADDING)) + val enabled = !dataUnchanged && canSaveProfile(displayName.value, profile) + val saveModifier: Modifier = Modifier.clickable(enabled) { saveProfile(displayName.value, fullName.value, profileImage.value) } + val saveColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary Text( stringResource(MR.strings.save_and_notify_contacts), modifier = saveModifier, @@ -216,6 +205,15 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { ) } +private fun isValidNewProfileName(displayName: String, profile: Profile): Boolean = + displayName == profile.displayName || isValidDisplayName(displayName.trim()) + +private fun showFullName(profile: Profile): Boolean = + profile.fullName.isNotEmpty() && profile.fullName != profile.displayName + +private fun canSaveProfile(displayName: String, profile: Profile): Boolean = + displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, profile) + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index 7929413c93..7d3239700f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -28,9 +28,8 @@ import chat.simplex.common.views.chatlist.UserProfilePickerItem import chat.simplex.common.views.chatlist.UserProfileRow import chat.simplex.common.views.database.PassphraseField import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.CreateProfile -import chat.simplex.common.model.* import chat.simplex.common.platform.appPlatform +import chat.simplex.common.views.CreateProfile import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.delay diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index d57211f4ac..7912a5ad1b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -675,7 +675,7 @@ Your contacts in SimpleX will see it.\nYou can change it in Settings. - Display name: + Profile name: Full name: Your current profile Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. @@ -703,11 +703,12 @@ Create profile Your profile, contacts and delivered messages are stored on your device. The profile is only shared with your contacts. - No spaces! Display name cannot contain whitespace. - Display Name - Full Name (optional) + Enter your name: Create + Create profile + Invalid name! + Correct name to %s? About SimpleX @@ -1290,7 +1291,7 @@ Create secret group The group is fully decentralized – it is visible only to the members. - Group display name: + Enter group name: Group full name: Your chat profile will be sent to group members From b05a45f55976e0466c71e651b21fe294b2941eb3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 7 Oct 2023 12:32:39 +0100 Subject: [PATCH 17/17] core: updated simplexmq (add tag to platform-specific sum type encoding that uses single field objects with swift/iOS) --- cabal.project | 4 ++-- scripts/nix/sha256map.nix | 4 ++-- stack.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cabal.project b/cabal.project index af664652db..3da442ac49 100644 --- a/cabal.project +++ b/cabal.project @@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: ec1b72cb8013a65a5d9783104a47ae44f5730089 + tag: 919550948501d315aa8845cbed1781d4298d4ced source-repository-package type: git @@ -34,7 +34,7 @@ source-repository-package source-repository-package type: git location: https://github.com/simplex-chat/aeson.git - tag: 68330dce8208173c6acf5f62b23acb500ab5d873 + tag: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index b6ca36e313..0a199779d0 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,10 +1,10 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ec1b72cb8013a65a5d9783104a47ae44f5730089" = "1lz5rvgxp242zg95r9zd9j50y45314cf8nfpjg1qsa55nrk2w19b"; + "https://github.com/simplex-chat/simplexmq.git"."919550948501d315aa8845cbed1781d4298d4ced" = "05d0cadhlazqi2lxcb7nvyjrf8q49c6ax7b8rahawbh1zmwg38nm"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; - "https://github.com/simplex-chat/aeson.git"."68330dce8208173c6acf5f62b23acb500ab5d873" = "1l51p1v54c88c1jmxcvbz4gy0cns7l46ihzzfjwxxrvcrrrxgcjp"; + "https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn"; "https://github.com/simplex-chat/network-transport.git"."0013798272a683e35ca38d2fdaf480942311fba8" = "0dnn62apgvc248df0m8ib7phrzn63wm0xs71xvlypv52j6cgwzkb"; diff --git a/stack.yaml b/stack.yaml index bce5dd3a68..5d9fc214fb 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: ec1b72cb8013a65a5d9783104a47ae44f5730089 + commit: 919550948501d315aa8845cbed1781d4298d4ced - github: kazu-yamamoto/http2 commit: b5a1b7200cf5bc7044af34ba325284271f6dff25 # - ../direct-sqlcipher @@ -60,7 +60,7 @@ extra-deps: commit: a46bd361a19376c5211f1058908fc0ae6bf42446 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson - commit: 68330dce8208173c6acf5f62b23acb500ab5d873 + commit: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b - github: simplex-chat/haskell-terminal commit: f708b00009b54890172068f168bf98508ffcd495 - github: simplex-chat/android-support