From 91fc238ddc95769ebcb6f32e63e08bb9bd6ada54 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 5 Oct 2023 00:06:23 +0800 Subject: [PATCH 1/2] desktop: libs refactoring (#3169) * desktop: libs refactoring * mac fix * windows fix * .gitignore * unused lines * desktop (windows): adapting Windows build to new libs * removed unused code --------- Co-authored-by: avently --- apps/multiplatform/.gitignore | 1 - .../src/commonMain/cpp/desktop/CMakeLists.txt | 7 +- .../simplex/common/platform/Files.desktop.kt | 2 - .../common/platform/Platform.desktop.kt | 12 +-- apps/multiplatform/desktop/build.gradle.kts | 91 ++++--------------- .../kotlin/chat/simplex/desktop/Main.kt | 42 ++------- scripts/desktop/build-lib-linux.sh | 24 ++++- scripts/desktop/build-lib-mac.sh | 28 ++++-- scripts/desktop/build-lib-windows.sh | 15 ++- scripts/desktop/download-lib-windows.sh | 13 ++- scripts/desktop/make-appimage-linux.sh | 9 +- scripts/desktop/prepare-vlc-linux.sh | 2 +- scripts/desktop/prepare-vlc-mac.sh | 2 +- scripts/desktop/prepare-vlc-windows.sh | 2 +- 14 files changed, 104 insertions(+), 146 deletions(-) diff --git a/apps/multiplatform/.gitignore b/apps/multiplatform/.gitignore index 81d296183..f30061200 100644 --- a/apps/multiplatform/.gitignore +++ b/apps/multiplatform/.gitignore @@ -11,7 +11,6 @@ local.properties common/src/commonMain/cpp/android/libs/ common/src/commonMain/cpp/desktop/libs/ -desktop/src/jvmMain/resources/libs/ android/build android/release common/build diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt index eb4794dd6..059e5af42 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt @@ -54,12 +54,11 @@ add_library( # Sets the name of the library. simplex-api.c) add_library( simplex SHARED IMPORTED ) -# Lib has different name because of version, find it -FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex*.${OS_LIB_EXT}) - if(WIN32) + FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex*.${OS_LIB_EXT}) set_target_properties( simplex PROPERTIES IMPORTED_IMPLIB ${SIMPLEXLIB}) else() + FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex-chat*.${OS_LIB_EXT}) set_target_properties( simplex PROPERTIES IMPORTED_LOCATION ${SIMPLEXLIB}) endif() @@ -72,7 +71,7 @@ if(NOT APPLE) else() # Without direct linking it can't find hs_init in linking step add_library( rts SHARED IMPORTED ) - FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/deps/libHSrts*_thr-*.${OS_LIB_EXT}) + FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libHSrts*_thr-*.${OS_LIB_EXT}) set_target_properties( rts PROPERTIES IMPORTED_LOCATION ${RTSLIB}) target_link_libraries(app-lib rts simplex) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt index 46124a44f..9042a6283 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt @@ -21,8 +21,6 @@ actual val agentDatabaseFileName: String = "simplex_v1_agent.db" actual val databaseExportDir: File = tmpDir -val vlcDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex-vlc").also { it.deleteOnExit() } - actual fun desktopOpenDatabaseDir() { if (Desktop.isDesktopSupported()) { try { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt index cb4e3acdb..9217551a8 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt @@ -8,12 +8,12 @@ private val unixConfigPath = (System.getenv("XDG_CONFIG_HOME") ?: "$home/.config private val unixDataPath = (System.getenv("XDG_DATA_HOME") ?: "$home/.local/share") + "/simplex" val desktopPlatform = detectDesktopPlatform() -enum class DesktopPlatform(val libPath: String, val libExtension: String, val configPath: String, val dataPath: String) { - LINUX_X86_64("/libs/linux-x86_64", "so", unixConfigPath, unixDataPath), - LINUX_AARCH64("/libs/aarch64", "so", unixConfigPath, unixDataPath), - WINDOWS_X86_64("/libs/windows-x86_64", "dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"), - MAC_X86_64("/libs/mac-x86_64", "dylib", unixConfigPath, unixDataPath), - MAC_AARCH64("/libs/mac-aarch64", "dylib", unixConfigPath, unixDataPath); +enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String) { + LINUX_X86_64("so", unixConfigPath, unixDataPath), + LINUX_AARCH64("so", unixConfigPath, unixDataPath), + WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"), + MAC_X86_64("dylib", unixConfigPath, unixDataPath), + MAC_AARCH64("dylib", unixConfigPath, unixDataPath); fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64 fun isWindows() = this == WINDOWS_X86_64 diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index 3da87850d..a7dab78ee 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -52,6 +52,7 @@ compose { } //includeAllModules = true outputBaseDir.set(project.file("../release")) + appResourcesRootDir.set(project.file("../build/links")) targetFormats( TargetFormat.Deb, TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe //, TargetFormat.AppImage // Gradle doesn't sync on Mac with it @@ -156,11 +157,10 @@ tasks.named("compileJava") { afterEvaluate { tasks.create("cmakeBuildAndCopy") { dependsOn("cmakeBuild") - val copyDetails = mutableMapOf>() doLast { copy { - from("${project(":desktop").buildDir}/cmake/main/linux-amd64", "$cppPath/desktop/libs/linux-x86_64", "$cppPath/desktop/libs/linux-x86_64/deps") - into("src/jvmMain/resources/libs/linux-x86_64") + from("${project(":desktop").buildDir}/cmake/main/linux-amd64") + into("$cppPath/desktop/libs/linux-x86_64") include("*.so*") eachFile { path = name @@ -169,16 +169,8 @@ afterEvaluate { duplicatesStrategy = DuplicatesStrategy.INCLUDE } copy { - val destinationDir = "src/jvmMain/resources/libs/linux-x86_64/vlc" - from("$cppPath/desktop/libs/linux-x86_64/deps/vlc") - into(destinationDir) - includeEmptyDirs = false - duplicatesStrategy = DuplicatesStrategy.INCLUDE - copyIfNeeded(destinationDir, copyDetails) - } - copy { - from("${project(":desktop").buildDir}/cmake/main/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64/deps") - into("src/jvmMain/resources/libs/linux-aarch64") + from("${project(":desktop").buildDir}/cmake/main/linux-aarch64") + into("$cppPath/desktop/libs/linux-aarch64") include("*.so*") eachFile { path = name @@ -187,16 +179,18 @@ afterEvaluate { duplicatesStrategy = DuplicatesStrategy.INCLUDE } copy { - val destinationDir = "src/jvmMain/resources/libs/linux-aarch64/vlc" - from("$cppPath/desktop/libs/linux-aarch64/deps/vlc") - into(destinationDir) + from("${project(":desktop").buildDir}/cmake/main/windows-amd64") + into("$cppPath/desktop/libs/windows-x86_64") + include("*.dll") + eachFile { + path = name + } includeEmptyDirs = false duplicatesStrategy = DuplicatesStrategy.INCLUDE - copyIfNeeded(destinationDir, copyDetails) } - copy { - from("${project(":desktop").buildDir}/cmake/main/windows-amd64", "$cppPath/desktop/libs/windows-x86_64", "$cppPath/desktop/libs/windows-x86_64/deps") - into("src/jvmMain/resources/libs/windows-x86_64") + copy { + from("${project(":desktop").buildDir}/cmake/main/windows-amd64") + into("../build/links/windows-x64") include("*.dll") eachFile { path = name @@ -205,16 +199,8 @@ afterEvaluate { duplicatesStrategy = DuplicatesStrategy.INCLUDE } copy { - val destinationDir = "src/jvmMain/resources/libs/windows-x86_64/vlc" - from("$cppPath/desktop/libs/windows-x86_64/deps/vlc") - into(destinationDir) - includeEmptyDirs = false - duplicatesStrategy = DuplicatesStrategy.INCLUDE - copyIfNeeded(destinationDir, copyDetails) - } - copy { - from("${project(":desktop").buildDir}/cmake/main/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64/deps") - into("src/jvmMain/resources/libs/mac-x86_64") + from("${project(":desktop").buildDir}/cmake/main/mac-x86_64") + into("$cppPath/desktop/libs/mac-x86_64") include("*.dylib") eachFile { path = name @@ -223,16 +209,8 @@ afterEvaluate { duplicatesStrategy = DuplicatesStrategy.INCLUDE } copy { - val destinationDir = "src/jvmMain/resources/libs/mac-x86_64/vlc" - from("$cppPath/desktop/libs/mac-x86_64/deps/vlc") - into(destinationDir) - includeEmptyDirs = false - duplicatesStrategy = DuplicatesStrategy.INCLUDE - copyIfNeeded(destinationDir, copyDetails) - } - copy { - from("${project(":desktop").buildDir}/cmake/main/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64/deps") - into("src/jvmMain/resources/libs/mac-aarch64") + from("${project(":desktop").buildDir}/cmake/main/mac-aarch64") + into("$cppPath/desktop/libs/mac-aarch64") include("*.dylib") eachFile { path = name @@ -240,39 +218,6 @@ afterEvaluate { includeEmptyDirs = false duplicatesStrategy = DuplicatesStrategy.INCLUDE } - copy { - val destinationDir = "src/jvmMain/resources/libs/mac-aarch64/vlc" - from("$cppPath/desktop/libs/mac-aarch64/deps/vlc") - into(destinationDir) - includeEmptyDirs = false - duplicatesStrategy = DuplicatesStrategy.INCLUDE - copyIfNeeded(destinationDir, copyDetails) - } - } - afterEvaluate { - doLast { - copyDetails.forEach { (destinationDir, details) -> - details.forEach { detail -> - val target = File(projectDir.absolutePath + File.separator + destinationDir + File.separator + detail.path) - if (target.exists()) { - target.setLastModified(detail.lastModified) - } - } - } - } } } } - -fun CopySpec.copyIfNeeded(destinationDir: String, into: MutableMap>) { - val details = arrayListOf() - eachFile { - val targetFile = File(destinationDir, path) - if (file.lastModified() == targetFile.lastModified() && file.length() == targetFile.length()) { - exclude() - } else { - details.add(this) - } - } - into[destinationDir] = details -} diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index a0be87732..ff92b08ff 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -18,51 +18,29 @@ fun main() { @Suppress("UnsafeDynamicallyLoadedCode") private fun initHaskell() { - val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs") - copyResources(desktopPlatform.libPath, libsTmpDir.toPath()) - vlcDir.deleteRecursively() - Files.move(File(libsTmpDir, "vlc").toPath(), vlcDir.toPath(), StandardCopyOption.REPLACE_EXISTING) + val resourcesDir = File(System.getProperty("compose.application.resources.dir")) + val vlcDir = File(resourcesDir.absolutePath + File.separator + "vlc") if (desktopPlatform == DesktopPlatform.WINDOWS_X86_64) { - windowsLoadRequiredLibs(libsTmpDir) + windowsLoadRequiredLibs(resourcesDir, vlcDir) } else { - System.load(File(libsTmpDir, "libapp-lib.${desktopPlatform.libExtension}").absolutePath) + System.load(File(resourcesDir, "libapp-lib.${desktopPlatform.libExtension}").absolutePath) } // No picture without preloading it, only sound. However, with libs from AppImage it works without preloading //val libXcb = "libvlc_xcb_events.so.0.0.0" //System.load(File(File(vlcDir, "vlc"), libXcb).absolutePath) System.setProperty("jna.library.path", vlcDir.absolutePath) //discoverVlcLibs(File(File(vlcDir, "vlc"), "plugins").absolutePath) - - libsTmpDir.deleteRecursively() initHS() } -private fun copyResources(from: String, to: Path) { - val resource = Class.forName("chat.simplex.desktop.MainKt").getResource("")!!.toURI() - val fileSystem = FileSystems.newFileSystem(resource, emptyMap()) - val resPath = fileSystem.getPath(from) - Files.walkFileTree(resPath, object: SimpleFileVisitor() { - override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { - Files.createDirectories(to.resolve(resPath.relativize(dir).toString())) - return FileVisitResult.CONTINUE - } - override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { - val dest = to.resolve(resPath.relativize(file).toString()) - Files.copy(file, dest, StandardCopyOption.REPLACE_EXISTING) - // Setting the same time on file as the time set in script that generates VLC libs - if (dest.toString().contains("." + desktopPlatform.libExtension)) { - dest.setLastModifiedTime(FileTime.fromMillis(0)) - } - return FileVisitResult.CONTINUE - } - }) -} - -private fun windowsLoadRequiredLibs(libsTmpDir: File) { +private fun windowsLoadRequiredLibs(libsTmpDir: File, vlcDir: File) { val mainLibs = arrayOf( "libcrypto-3-x64.dll", + "mcfgthread-12.dll", + "libgcc_s_seh-1.dll", + "libstdc++-6.dll", "libffi-8.dll", - "libgmp-10.dll", + "libgmp-10.dll", "libsimplex.dll", "libapp-lib.dll" ) @@ -72,7 +50,7 @@ private fun windowsLoadRequiredLibs(libsTmpDir: File) { val vlcLibs = arrayOf( "libvlccore.dll", "libvlc.dll", - "axvlc.dll", + "axvlc.dll", "npvlc.dll" ) vlcLibs.forEach { diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index 2d9681fc6..e0ee7e669 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -1,9 +1,23 @@ #!/bin/bash +set -e + +function readlink() { + echo "$(cd "$(dirname "$1")"; pwd -P)" +} + OS=linux ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`} GHC_VERSION=9.6.2 +if [ "$ARCH" == "aarch64" ]; then + COMPOSE_ARCH=arm64 +else + COMPOSE_ARCH=x64 +fi + +root_dir="$(dirname "$(dirname "$(readlink "$0")")")" +cd $root_dir BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-* rm -rf $BUILD_DIR @@ -11,16 +25,20 @@ cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts - cd $BUILD_DIR/build #patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so #patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so -mkdir deps +mkdir deps 2> /dev/null || true ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/ cd - rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ scripts/desktop/prepare-vlc-linux.sh + +links_dir=apps/multiplatform/build/links +mkdir -p $links_dir +cd $links_dir +ln -sfT ../../common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ $OS-$COMPOSE_ARCH diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index 303e33154..3680a4a2a 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -4,10 +4,13 @@ set -e OS=mac ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}" +COMPOSE_ARCH=$ARCH GHC_VERSION=9.6.2 if [ "$ARCH" == "arm64" ]; then ARCH=aarch64 +else + COMPOSE_ARCH=x64 fi LIB_EXT=dylib @@ -84,30 +87,29 @@ rm deps/`basename $LIB` cd - rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -LIBCRYPTO_PATH=$(otool -l deps/libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) -install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSdrct-*.$LIB_EXT -cp $LIBCRYPTO_PATH deps/libcrypto.1.1.$LIB_EXT -chmod 755 deps/libcrypto.1.1.$LIB_EXT -install_name_tool -id "libcrypto.1.1.$LIB_EXT" deps/libcrypto.1.1.$LIB_EXT -install_name_tool -id "libffi.8.$LIB_EXT" deps/libffi.$LIB_EXT +LIBCRYPTO_PATH=$(otool -l libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) +install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSdrct-*.$LIB_EXT +cp $LIBCRYPTO_PATH libcrypto.1.1.$LIB_EXT +chmod 755 libcrypto.1.1.$LIB_EXT +install_name_tool -id "libcrypto.1.1.$LIB_EXT" libcrypto.1.1.$LIB_EXT +install_name_tool -id "libffi.8.$LIB_EXT" libffi.$LIB_EXT LIBCRYPTO_PATH=$(otool -l $LIB | grep libcrypto | cut -d' ' -f11) if [ -n "$LIBCRYPTO_PATH" ]; then install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT $LIB fi -LIBCRYPTO_PATH=$(otool -l deps/libHSsmplxmq*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) +LIBCRYPTO_PATH=$(otool -l libHSsmplxmq*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) if [ -n "$LIBCRYPTO_PATH" ]; then - install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSsmplxmq*.$LIB_EXT + install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSsmplxmq*.$LIB_EXT fi for lib in $(find . -type f -name "*.$LIB_EXT"); do @@ -126,3 +128,9 @@ fi cd - scripts/desktop/prepare-vlc-mac.sh + +links_dir=apps/multiplatform/build/links +mkdir -p $links_dir +cd $links_dir +rm macos-$COMPOSE_ARCH 2>/dev/null | true +ln -sf ../../common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ macos-$COMPOSE_ARCH diff --git a/scripts/desktop/build-lib-windows.sh b/scripts/desktop/build-lib-windows.sh index 658324baa..ef39ef868 100755 --- a/scripts/desktop/build-lib-windows.sh +++ b/scripts/desktop/build-lib-windows.sh @@ -8,15 +8,26 @@ function readlink() { root_dir="$(dirname "$(dirname "$(readlink "$0")")")" OS=windows -ARCH=`uname -a | rev | cut -d' ' -f2 | rev` +ARCH="x86_64" JOB_REPO=${1:-$SIMPLEX_CI_REPO_URL} +if [ "$ARCH" == "aarch64" ]; then + COMPOSE_ARCH=arm64 +else + COMPOSE_ARCH=x64 +fi + cd $root_dir rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ scripts/desktop/download-lib-windows.sh $JOB_REPO scripts/desktop/prepare-vlc-windows.sh + +links_dir=apps/multiplatform/build/links +mkdir -p $links_dir +cd $links_dir +rm -rf $OS-$COMPOSE_ARCH +ln -sfT ../../common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ $OS-$COMPOSE_ARCH diff --git a/scripts/desktop/download-lib-windows.sh b/scripts/desktop/download-lib-windows.sh index 14439274c..945bd7b5e 100644 --- a/scripts/desktop/download-lib-windows.sh +++ b/scripts/desktop/download-lib-windows.sh @@ -7,7 +7,7 @@ function readlink() { } if [ -z "${1}" ]; then - echo "Job repo is unset. Provide it via first argument like: $(readlink "$0")/download-lib-windows.sh https://something.com/job/something/{windows,windows-8107}" + echo "Job repo is unset. Provide it via first argument like: $(readlink "$0")/download-lib-windows.sh https://something.com/job/something/{master,stable}" exit 1 fi @@ -16,12 +16,15 @@ arch=x86_64 root_dir="$(dirname "$(dirname "$(readlink "$0")")")" output_dir="$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-$arch/" -mkdir -p "$output_dir"/deps 2> /dev/null +mkdir -p "$output_dir" 2> /dev/null curl --location -o libsimplex.zip $job_repo/$arch-linux.$arch-windows:lib:simplex-chat/latest/download/1 && \ $WINDIR\\System32\\tar.exe -xf libsimplex.zip && \ mv libsimplex.dll "$output_dir" && \ -mv libcrypto*.dll "$output_dir/deps" && \ -mv libffi*.dll "$output_dir/deps" && \ -mv libgmp*.dll "$output_dir/deps" && \ +mv libcrypto*.dll "$output_dir" && \ +mv libffi*.dll "$output_dir" && \ +mv libgmp*.dll "$output_dir" && \ +mv mcfgthread*.dll "$output_dir" && \ +mv libgcc_s_seh*.dll "$output_dir" && \ +mv libstdc++*.dll "$output_dir" && \ rm libsimplex.zip diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index 35e62481d..9cd6f525f 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -11,13 +11,12 @@ multiplatform_dir=$root_dir/apps/multiplatform release_app_dir=$root_dir/apps/multiplatform/release/main/app cd $multiplatform_dir -libcrypto_path=$(ldd common/src/commonMain/cpp/desktop/libs/*/deps/libHSdirect-sqlcipher-*.so | grep libcrypto | cut -d'=' -f 2 | cut -d ' ' -f 2) -trap "rm common/src/commonMain/cpp/desktop/libs/*/deps/`basename $libcrypto_path` 2> /dev/null || true" EXIT -cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/*/deps +libcrypto_path=$(ldd common/src/commonMain/cpp/desktop/libs/*/libHSdirect-sqlcipher-*.so | grep libcrypto | cut -d'=' -f 2 | cut -d ' ' -f 2) +trap "rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path` 2> /dev/null || true" EXIT +cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/* ./gradlew createDistributable -rm common/src/commonMain/cpp/desktop/libs/*/deps/`basename $libcrypto_path` -rm desktop/src/jvmMain/resources/libs/*/`basename $libcrypto_path` +rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path` rm -rf $release_app_dir/AppDir 2>/dev/null mkdir -p $release_app_dir/AppDir/usr diff --git a/scripts/desktop/prepare-vlc-linux.sh b/scripts/desktop/prepare-vlc-linux.sh index e1cfa7e9f..a76486150 100755 --- a/scripts/desktop/prepare-vlc-linux.sh +++ b/scripts/desktop/prepare-vlc-linux.sh @@ -6,7 +6,7 @@ function readlink() { echo "$(cd "$(dirname "$1")"; pwd -P)" } root_dir="$(dirname "$(dirname "$(readlink "$0")")")" -vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linux-x86_64/deps/vlc +vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linux-x86_64/vlc mkdir $vlc_dir || exit 0 diff --git a/scripts/desktop/prepare-vlc-mac.sh b/scripts/desktop/prepare-vlc-mac.sh index 69644bcc1..25ec1365f 100755 --- a/scripts/desktop/prepare-vlc-mac.sh +++ b/scripts/desktop/prepare-vlc-mac.sh @@ -16,7 +16,7 @@ function readlink() { } root_dir="$(dirname "$(dirname "$(readlink "$0")")")" -vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/mac-$ARCH/deps/vlc +vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/mac-$ARCH/vlc #rm -rf $vlc_dir mkdir -p $vlc_dir/vlc || exit 0 diff --git a/scripts/desktop/prepare-vlc-windows.sh b/scripts/desktop/prepare-vlc-windows.sh index bdb492344..680fa7b80 100644 --- a/scripts/desktop/prepare-vlc-windows.sh +++ b/scripts/desktop/prepare-vlc-windows.sh @@ -6,7 +6,7 @@ function readlink() { echo "$(cd "$(dirname "$1")"; pwd -P)" } root_dir="$(dirname "$(dirname "$(readlink "$0")")")" -vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-x86_64/deps/vlc +vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-x86_64/vlc rm -rf $vlc_dir mkdir -p $vlc_dir/vlc || exit 0 From 0d8558a6d04688e6ed000ec73b8c1a54ba17508b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 4 Oct 2023 17:45:39 +0100 Subject: [PATCH 2/2] ios: profile names (remove full name) (#3168) * ios: profile names (remove full name) * create/update groups * focus display name --- .../Views/Chat/Group/GroupProfileView.swift | 67 +++- .../Shared/Views/NewChat/AddGroupView.swift | 41 +-- .../Views/Onboarding/CreateProfile.swift | 341 +++++++++++------- .../Views/Onboarding/OnboardingView.swift | 2 +- .../Views/UserSettings/SettingsView.swift | 4 +- .../Views/UserSettings/UserProfile.swift | 51 ++- apps/ios/SimpleXChat/SimpleX.h | 1 + 7 files changed, 315 insertions(+), 192 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 729556e73..6ff3af66a 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -9,6 +9,18 @@ import SwiftUI import SimpleXChat +enum GroupProfileAlert: Identifiable { + case saveError(err: String) + case invalidName(validName: String) + + var id: String { + switch self { + case let .saveError(err): return "saveError \(err)" + case let .invalidName(validName): return "invalidName \(validName)" + } + } +} + struct GroupProfileView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss: DismissAction @@ -18,8 +30,7 @@ struct GroupProfileView: View { @State private var showImagePicker = false @State private var showTakePhoto = false @State private var chosenImage: UIImage? = nil - @State private var showSaveErrorAlert = false - @State private var saveGroupError: String? = nil + @State private var alert: GroupProfileAlert? @FocusState private var focusDisplayName var body: some View { @@ -47,20 +58,29 @@ struct GroupProfileView: View { .frame(maxWidth: .infinity, alignment: .center) VStack(alignment: .leading) { - ZStack(alignment: .leading) { - if !validDisplayName(groupProfile.displayName) { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .padding(.bottom, 10) + ZStack(alignment: .topLeading) { + if !validNewProfileName() { + Button { + alert = .invalidName(validName: mkValidName(groupProfile.displayName)) + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } else { + Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } profileNameTextEdit("Group display name", $groupProfile.displayName) .focused($focusDisplayName) } - profileNameTextEdit("Group full name (optional)", $groupProfile.fullName) + .padding(.bottom) + let fullName = groupInfo.groupProfile.fullName + if fullName != "" && fullName != groupProfile.displayName { + profileNameTextEdit("Group full name (optional)", $groupProfile.fullName) + .padding(.bottom) + } HStack(spacing: 20) { Button("Cancel") { dismiss() } Button("Save group profile") { saveProfile() } - .disabled(groupProfile.displayName == "" || !validDisplayName(groupProfile.displayName)) + .disabled(groupProfile.displayName == "" || !validNewProfileName()) } } .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) @@ -99,27 +119,35 @@ struct GroupProfileView: View { focusDisplayName = true } } - .alert(isPresented: $showSaveErrorAlert) { - Alert( - title: Text("Error saving group profile"), - message: Text("\(saveGroupError ?? "Unexpected error")") - ) + .alert(item: $alert) { a in + switch a { + case let .saveError(err): + return Alert( + title: Text("Error saving group profile"), + message: Text(err) + ) + case let .invalidName(name): + return createInvalidNameAlert(name, $groupProfile.displayName) + } } .contentShape(Rectangle()) .onTapGesture { hideKeyboard() } } + private func validNewProfileName() -> Bool { + groupProfile.displayName == groupInfo.groupProfile.displayName + || validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces)) + } + func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding) -> some View { TextField(label, text: name) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .padding(.bottom) - .padding(.leading, 28) + .padding(.leading, 32) } func saveProfile() { Task { do { + groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces) let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile) await MainActor.run { groupInfo = gInfo @@ -128,8 +156,7 @@ struct GroupProfileView: View { } } catch let error { let err = responseError(error) - saveGroupError = err - showSaveErrorAlert = true + alert = .saveError(err: err) logger.error("GroupProfile apiUpdateGroup error: \(err)") } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 8df37bb56..5cea52cc8 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -16,11 +16,11 @@ struct AddGroupView: View { @State private var groupInfo: GroupInfo? @State private var profile = GroupProfile(displayName: "", fullName: "") @FocusState private var focusDisplayName - @FocusState private var focusFullName @State private var showChooseSource = false @State private var showImagePicker = false @State private var showTakePhoto = false @State private var chosenImage: UIImage? = nil + @State private var showInvalidNameAlert = false var body: some View { if let chat = chat, let groupInfo = groupInfo { @@ -76,26 +76,24 @@ struct AddGroupView: View { .padding(.bottom, 4) ZStack(alignment: .topLeading) { - if !validDisplayName(profile.displayName) { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .padding(.top, 4) + let name = profile.displayName.trimmingCharacters(in: .whitespaces) + if name != mkValidName(name) { + Button { + showInvalidNameAlert = true + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } else { + Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } - textField("Group display name", text: $profile.displayName) + textField("Enter group name…", text: $profile.displayName) .focused($focusDisplayName) - .submitLabel(.next) + .submitLabel(.go) .onSubmit { - if canCreateProfile() { focusFullName = true } - else { focusDisplayName = true } + if canCreateProfile() { createGroup() } } } - textField("Group full name (optional)", text: $profile.fullName) - .focused($focusFullName) - .submitLabel(.go) - .onSubmit { - if canCreateProfile() { createGroup() } - else { focusFullName = true } - } + .padding(.bottom) Spacer() @@ -133,6 +131,9 @@ struct AddGroupView: View { didSelectItem in showImagePicker = false } } + .alert(isPresented: $showInvalidNameAlert) { + createInvalidNameAlert(mkValidName(profile.displayName), $profile.displayName) + } .onChange(of: chosenImage) { image in if let image = image { profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) @@ -146,15 +147,13 @@ struct AddGroupView: View { func textField(_ placeholder: LocalizedStringKey, text: Binding) -> some View { TextField(placeholder, text: text) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .padding(.leading, 28) - .padding(.bottom) + .padding(.leading, 32) } func createGroup() { hideKeyboard() do { + profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) let gInfo = try apiNewGroup(profile) Task { let groupMembers = await apiListMembers(gInfo.groupId) @@ -180,7 +179,7 @@ struct AddGroupView: View { } func canCreateProfile() -> Bool { - profile.displayName != "" && validDisplayName(profile.displayName) + profile.displayName != "" && validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces)) } } diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index d05ac4458..f5db37dac 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -9,175 +9,244 @@ import SwiftUI import SimpleXChat +enum UserProfileAlert: Identifiable { + case duplicateUserError + case createUserError(error: LocalizedStringKey) + case invalidNameError(validName: String) + + var id: String { + switch self { + case .duplicateUserError: return "duplicateUserError" + case .createUserError: return "createUserError" + case let .invalidNameError(validName): return "invalidNameError \(validName)" + } + } +} + struct CreateProfile: View { + @Environment(\.dismiss) var dismiss + @State private var displayName: String = "" + @FocusState private var focusDisplayName + @State private var alert: UserProfileAlert? + + var body: some View { + List { + Section { + TextField("Enter your name…", text: $displayName) + .focused($focusDisplayName) + Button { + createProfile(displayName, showAlert: { alert = $0 }, dismiss: dismiss) + } label: { + Label("Create profile", systemImage: "checkmark") + } + .disabled(!canCreateProfile(displayName)) + } header: { + HStack { + Text("Your profile") + let name = displayName.trimmingCharacters(in: .whitespaces) + let validName = mkValidName(name) + if name != validName { + Spacer() + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + .onTapGesture { + alert = .invalidNameError(validName: validName) + } + } + } + .frame(height: 20) + } footer: { + VStack(alignment: .leading, spacing: 8) { + Text("Your profile, contacts and delivered messages are stored on your device.") + Text("The profile is only shared with your contacts.") + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .navigationTitle("Create your profile") + .alert(item: $alert) { a in userProfileAlert(a, $displayName) } + .onAppear() { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + focusDisplayName = true + } + } + .keyboardPadding() + } +} + +struct CreateFirstProfile: View { @EnvironmentObject var m: ChatModel @Environment(\.dismiss) var dismiss @State private var displayName: String = "" - @State private var fullName: String = "" @FocusState private var focusDisplayName - @FocusState private var focusFullName - @State private var alert: CreateProfileAlert? - - private enum CreateProfileAlert: Identifiable { - case duplicateUserError - case createUserError(error: LocalizedStringKey) - - var id: String { - switch self { - case .duplicateUserError: return "duplicateUserError" - case .createUserError: return "createUserError" - } - } - } var body: some View { VStack(alignment: .leading) { - Text("Create your profile") - .font(.largeTitle) - .bold() - .padding(.bottom, 4) - .frame(maxWidth: .infinity) - Text("Your profile, contacts and delivered messages are stored on your device.") - .padding(.bottom, 4) - Text("The profile is only shared with your contacts.") - .padding(.bottom) + Group { + Text("Create your profile") + .font(.largeTitle) + .bold() + Text("Your profile, contacts and delivered messages are stored on your device.") + .foregroundColor(.secondary) + Text("The profile is only shared with your contacts.") + .foregroundColor(.secondary) + .padding(.bottom) + } + .padding(.bottom) + ZStack(alignment: .topLeading) { - if !validDisplayName(displayName) { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .padding(.top, 4) + let name = displayName.trimmingCharacters(in: .whitespaces) + let validName = mkValidName(name) + if name != validName { + Button { + showAlert(.invalidNameError(validName: validName)) + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } else { + Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } - textField("Display name", text: $displayName) + TextField("Enter your name…", text: $displayName) .focused($focusDisplayName) - .submitLabel(.next) - .onSubmit { - if canCreateProfile() { focusFullName = true } - else { focusDisplayName = true } - } + .padding(.leading, 32) } - textField("Full name (optional)", text: $fullName) - .focused($focusFullName) - .submitLabel(.go) - .onSubmit { - if canCreateProfile() { createProfile() } - else { focusFullName = true } - } - + .padding(.bottom) Spacer() - - HStack { - if m.users.isEmpty { - Button { - hideKeyboard() - withAnimation { - m.onboardingStage = .step1_SimpleXInfo - } - } label: { - HStack { - Image(systemName: "lessthan") - Text("About SimpleX") - } - } - } - - Spacer() - - HStack { - Button { - createProfile() - } label: { - Text("Create") - Image(systemName: "greaterthan") - } - .disabled(!canCreateProfile()) - } - } + onboardingButtons() } .onAppear() { focusDisplayName = true setLastVersionDefault() } - .alert(item: $alert) { a in - switch a { - case .duplicateUserError: return duplicateUserAlert - case let .createUserError(err): return creatUserErrorAlert(err) - } - } .padding() + .frame(maxWidth: .infinity, alignment: .leading) .keyboardPadding() } - func textField(_ placeholder: LocalizedStringKey, text: Binding) -> some View { - TextField(placeholder, text: text) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .padding(.leading, 28) - .padding(.bottom) - } - - func createProfile() { - hideKeyboard() - let profile = Profile( - displayName: displayName, - fullName: fullName - ) - do { - m.currentUser = try apiCreateActiveUser(profile) - if m.users.isEmpty { - try startChat() + func onboardingButtons() -> some View { + HStack { + Button { + hideKeyboard() withAnimation { - onboardingStageDefault.set(.step3_CreateSimpleXAddress) - m.onboardingStage = .step3_CreateSimpleXAddress + m.onboardingStage = .step1_SimpleXInfo } - } else { - onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete - dismiss() - m.users = try listUsers() - try getUserChatData() - } - } catch let error { - switch error as? ChatResponse { - case .chatCmdError(_, .errorStore(.duplicateName)), - .chatCmdError(_, .error(.userExists)): - if m.currentUser == nil { - AlertManager.shared.showAlert(duplicateUserAlert) - } else { - alert = .duplicateUserError - } - default: - let err: LocalizedStringKey = "Error: \(responseError(error))" - if m.currentUser == nil { - AlertManager.shared.showAlert(creatUserErrorAlert(err)) - } else { - alert = .createUserError(error: err) + } label: { + HStack { + Image(systemName: "lessthan") + Text("About SimpleX") } } - logger.error("Failed to create user or start chat: \(responseError(error))") + + Spacer() + + Button { + createProfile(displayName, showAlert: showAlert, dismiss: dismiss) + } label: { + HStack { + Text("Create") + Image(systemName: "greaterthan") + } + } + .disabled(!canCreateProfile(displayName)) } } - func canCreateProfile() -> Bool { - displayName != "" && validDisplayName(displayName) - } - - private var duplicateUserAlert: Alert { - Alert( - title: Text("Duplicate display name!"), - message: Text("You already have a chat profile with the same display name. Please choose another name.") - ) - } - - private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert { - Alert( - title: Text("Error creating profile!"), - message: Text(err) - ) + private func showAlert(_ alert: UserProfileAlert) { + AlertManager.shared.showAlert(userProfileAlert(alert, $displayName)) } } +private func createProfile(_ displayName: String, showAlert: (UserProfileAlert) -> Void, dismiss: DismissAction) { + hideKeyboard() + let profile = Profile( + displayName: displayName.trimmingCharacters(in: .whitespaces), + fullName: "" + ) + let m = ChatModel.shared + do { + m.currentUser = try apiCreateActiveUser(profile) + if m.users.isEmpty { + try startChat() + withAnimation { + onboardingStageDefault.set(.step3_CreateSimpleXAddress) + m.onboardingStage = .step3_CreateSimpleXAddress + } + } else { + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete + dismiss() + m.users = try listUsers() + try getUserChatData() + } + } catch let error { + switch error as? ChatResponse { + case .chatCmdError(_, .errorStore(.duplicateName)), + .chatCmdError(_, .error(.userExists)): + if m.currentUser == nil { + AlertManager.shared.showAlert(duplicateUserAlert) + } else { + showAlert(.duplicateUserError) + } + default: + let err: LocalizedStringKey = "Error: \(responseError(error))" + if m.currentUser == nil { + AlertManager.shared.showAlert(creatUserErrorAlert(err)) + } else { + showAlert(.createUserError(error: err)) + } + } + logger.error("Failed to create user or start chat: \(responseError(error))") + } +} + +private func canCreateProfile(_ displayName: String) -> Bool { + let name = displayName.trimmingCharacters(in: .whitespaces) + return name != "" && mkValidName(name) == name +} + +func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding) -> Alert { + switch alert { + case .duplicateUserError: return duplicateUserAlert + case let .createUserError(err): return creatUserErrorAlert(err) + case let .invalidNameError(name): return createInvalidNameAlert(name, displayName) + } +} + +private var duplicateUserAlert: Alert { + Alert( + title: Text("Duplicate display name!"), + message: Text("You already have a chat profile with the same display name. Please choose another name.") + ) +} + +private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert { + Alert( + title: Text("Error creating profile!"), + message: Text(err) + ) +} + +func createInvalidNameAlert(_ name: String, _ displayName: Binding) -> Alert { + name == "" + ? Alert(title: Text("Invalid name!")) + : Alert( + title: Text("Invalid name!"), + message: Text("Correct name to \(name)?"), + primaryButton: .default( + Text("Ok"), + action: { displayName.wrappedValue = name } + ), + secondaryButton: .cancel() + ) +} + func validDisplayName(_ name: String) -> Bool { - name.firstIndex(of: " ") == nil && name.first != "@" && name.first != "#" + mkValidName(name.trimmingCharacters(in: .whitespaces)) == name +} + +func mkValidName(_ s: String) -> String { + var c = s.cString(using: .utf8)! + return fromCString(chat_valid_name(&c)!) } struct CreateProfile_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index b0734be64..438491b5f 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -14,7 +14,7 @@ struct OnboardingView: View { var body: some View { switch onboarding { case .step1_SimpleXInfo: SimpleXInfo(onboarding: true) - case .step2_CreateProfile: CreateProfile() + case .step2_CreateProfile: CreateFirstProfile() case .step3_CreateSimpleXAddress: CreateSimpleXAddress() case .step4_SetNotificationsMode: SetNotificationsMode() case .onboardingComplete: EmptyView() diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index d6681a51c..f076a4eb7 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -381,7 +381,9 @@ struct ProfilePreview: View { Text(profileOf.displayName) .fontWeight(.bold) .font(.title2) - Text(profileOf.fullName) + if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName { + Text(profileOf.fullName) + } } } } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index f38dc593a..88be0b6a7 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -17,6 +17,8 @@ struct UserProfile: View { @State private var showImagePicker = false @State private var showTakePhoto = false @State private var chosenImage: UIImage? = nil + @State private var alert: UserProfileAlert? + @FocusState private var focusDisplayName var body: some View { let user: User = chatModel.currentUser! @@ -47,18 +49,27 @@ struct UserProfile: View { VStack(alignment: .leading) { ZStack(alignment: .leading) { - if !validDisplayName(profile.displayName) { - Image(systemName: "exclamationmark.circle") - .foregroundColor(.red) - .padding(.bottom, 10) + if !validNewProfileName(user) { + Button { + alert = .invalidNameError(validName: mkValidName(profile.displayName)) + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } else { + Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } - profileNameTextEdit("Display name", $profile.displayName) + profileNameTextEdit("Profile name", $profile.displayName) + .focused($focusDisplayName) + } + .padding(.bottom) + if showFullName(user) { + profileNameTextEdit("Full name (optional)", $profile.fullName) + .padding(.bottom) } - profileNameTextEdit("Full name (optional)", $profile.fullName) HStack(spacing: 20) { Button("Cancel") { editProfile = false } Button("Save (and notify contacts)") { saveProfile() } - .disabled(profile.displayName == "" || !validDisplayName(profile.displayName)) + .disabled(!canSaveProfile(user)) } } .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) @@ -74,11 +85,14 @@ struct UserProfile: View { .frame(maxWidth: .infinity, alignment: .center) VStack(alignment: .leading) { - profileNameView("Display name:", user.profile.displayName) - profileNameView("Full name:", user.profile.fullName) + profileNameView("Profile name:", user.profile.displayName) + if showFullName(user) { + profileNameView("Full name:", user.profile.fullName) + } Button("Edit") { profile = fromLocalProfile(user.profile) editProfile = true + focusDisplayName = true } } .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) @@ -117,14 +131,12 @@ struct UserProfile: View { profile.image = nil } } + .alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) } } func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding) -> some View { TextField(label, text: name) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .padding(.bottom) - .padding(.leading, 28) + .padding(.leading, 32) } func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View { @@ -141,9 +153,22 @@ struct UserProfile: View { showChooseSource = true } + private func validNewProfileName(_ user: User) -> Bool { + profile.displayName == user.profile.displayName || validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces)) + } + + private func showFullName(_ user: User) -> Bool { + user.profile.fullName != "" && user.profile.fullName != user.profile.displayName + } + + private func canSaveProfile(_ user: User) -> Bool { + profile.displayName != "" && validNewProfileName(user) + } + func saveProfile() { Task { do { + profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) if let (newProfile, _) = try await apiUpdateProfile(profile: profile) { DispatchQueue.main.async { chatModel.updateCurrentUser(newProfile) diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 67c2fa728..9db3f06ae 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -23,6 +23,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); extern char *chat_password_hash(char *pwd, char *salt); +extern char *chat_valid_name(char *name); extern char *chat_encrypt_media(char *key, char *frame, int len); extern char *chat_decrypt_media(char *key, char *frame, int len);