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: 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/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/cpp/desktop/CMakeLists.txt b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt index 5cbc6883a..e4fddeeb7 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/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..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 ) } } @@ -122,9 +131,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}") } } @@ -628,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, @@ -690,7 +683,8 @@ fun PreviewChatListNavLinkDirect() { click = {}, dropdownMenuItems = null, showMenu = remember { mutableStateOf(false) }, - stopped = false + stopped = false, + selectedChat = remember { mutableStateOf(false) } ) } } @@ -730,7 +724,8 @@ fun PreviewChatListNavLinkGroup() { click = {}, dropdownMenuItems = null, showMenu = remember { mutableStateOf(false) }, - stopped = false + stopped = false, + selectedChat = remember { mutableStateOf(false) } ) } } @@ -750,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/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/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() +} 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..3da87850d 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -63,9 +63,10 @@ compose { windows { packageName = "SimpleX" iconFile.set(project.file("src/jvmMain/resources/distribute/simplex.ico")) - console = true - perUserInstall = true + console = false + perUserInstall = false dirChooser = true + shortcut = true } macOS { packageName = "SimpleX" @@ -119,9 +120,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 +140,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 +195,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/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 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 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 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 diff --git a/simplex-chat.cabal b/simplex-chat.cabal index b79c57068..9f6594c0a 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 267959bab..cb8760add 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -28,7 +28,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') @@ -355,6 +355,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 @@ -891,7 +892,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 @@ -1448,7 +1450,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 @@ -1953,9 +1956,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 <- @@ -1997,8 +2001,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' @@ -2007,6 +2012,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 (memberRole (membership :: GroupMember) < requiredRole) $ throwChatError $ CEGroupUserRole g requiredRole @@ -5235,8 +5244,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 @@ -5266,10 +5274,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 @@ -5600,7 +5611,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)) @@ -5613,7 +5630,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 @@ -5708,3 +5724,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 f363d020f..778301a83 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -14,7 +14,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) @@ -223,7 +223,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 @@ -673,10 +673,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] @@ -729,14 +726,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] @@ -753,20 +750,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] @@ -787,11 +784,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 @@ -841,7 +838,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 @@ -850,7 +847,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"] @@ -860,10 +857,10 @@ viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g GroupName) . fst - 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" @@ -871,18 +868,18 @@ viewGroupsList gs = map groupSS $ sortOn ldn_ 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 @@ -890,21 +887,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" ] @@ -1050,18 +1047,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 = @@ -1072,10 +1069,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 @@ -1201,9 +1198,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} @@ -1380,10 +1377,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] @@ -1550,6 +1547,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"] @@ -1562,8 +1562,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"] @@ -1575,7 +1575,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"] @@ -1635,8 +1635,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"] @@ -1680,8 +1680,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 @@ -1696,7 +1696,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 @@ -1716,37 +1716,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 @@ -1767,8 +1776,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 @@ -1777,12 +1785,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"