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/13] 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 4bb06afd8..43043d65f 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 e47134166..c4f2a2cbd 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 68aa1fff9..a6ac8c14e 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 8969e48b2..eebe7b3f4 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 26e9948d6..d57211f4a 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 000000000..084853704 --- /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 62a4d9e21..6b81209d4 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/13] 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 406f9aaba..907a2a068 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 dc3a23adc..ac641f841 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/13] 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 09ef6fd53..eb4794dd6 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 2121b9cfd..cb4e3acdb 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 ed8efcd57..4439680c6 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 20675dc74..79fcda7a5 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 21b2cfa6e..2a9042c43 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 a4e8207a1..bd9cb68d8 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 72c41a665..a0be87732 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 f67cc397a..4702f0360 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 000000000..658324baa --- /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 000000000..14439274c --- /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 000000000..bdb492344 --- /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/13] 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 20ba15e92..8b45e45f9 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 0d047a791..e37dbca8e 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/13] 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 810fd6f4c..64b76e7fe 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/13] 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 4525f3c5d..039c13646 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/13] 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 2a568d628..5b56c0ac8 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/13] 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 0eb02515b..8687ac390 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 b22b2f91c..ee9e109a7 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 7a0fc1278..4d95431a3 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/13] 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 000000000..3f33913e5 --- /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 4d95431a3..41c94f21d 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 000000000..0ad69d1c0 --- /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/13] 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 bd9cb68d8..3da87850d 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/13] 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 8928e9724..fc924a870 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/13] 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 ac641f841..0b613310f 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 5b56c0ac8..5d370ee65 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 15c06cba9..61840b8e8 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 700548bb1..783e38ef3 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 01bdfba95..5eb9df3ad 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 44af70a65..da6cbd156 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 c120d661f..6831cf319 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 0b965250b..69c2207ff 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 455d5459c..cf60a7013 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 000000000..40cda0143 --- /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/13] 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 10faa1a82..1bc965849 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 8bb70c4a0..3aa4a9261 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 95b6a73ca..fa99d0f93 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 c4f2a2cbd..a471b5645 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 ee9e109a7..4afcdacbd 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 c6e6ca7b7..972bc6621 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 2d696b778..28882e6b7 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 b5236e249..dd07a3fc1 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 3b7ba8486..c677edc06 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 c1d9eeec5..9df5bd0a1 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)) + } +}