From 2f10633d1d51b98eefd3309290139b0716efa401 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 2 Aug 2023 19:20:15 +0300 Subject: [PATCH 1/8] multiplatform: update compose version (#2835) * multiplatform: update compose version * Revert "desktop: fix freeze in some situations (#2820)" This reverts commit 6268f0a32b1bb35aeeb2eba4c1d14c80d3a4ba8f. * Revert "desktop: prevent deadlock (#2785)" This reverts commit d77980e50e27af2bbabb76f933ae75c1ca92ceee. * restore debug commands in comments --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../chat/simplex/common/model/ChatModel.kt | 17 +++++++---------- .../chat/simplex/common/model/SimpleXAPI.kt | 4 +--- .../chat/simplex/common/views/TerminalView.kt | 10 +++++----- .../simplex/common/views/chat/ChatInfoView.kt | 2 +- apps/multiplatform/gradle.properties | 2 +- 5 files changed, 15 insertions(+), 20 deletions(-) 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 39740d05fc..7a036fffdb 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 @@ -49,14 +49,14 @@ object ChatModel { val chatDbStatus = mutableStateOf(null) val chats = mutableStateListOf() // map of connections network statuses, key is agent connection id - val networkStatuses = mutableStateOf>(mapOf()) + val networkStatuses = mutableStateMapOf() // current chat val chatId = mutableStateOf(null) val chatItems = mutableStateListOf() val groupMembers = mutableStateListOf() - val terminalItems = mutableStateOf(emptyList()) + val terminalItems = mutableStateListOf() val userAddress = mutableStateOf(null) // Allows to temporary save servers that are being edited on multiple screens val userSMPServersUnsaved = mutableStateOf<(List)?>(null) @@ -477,20 +477,17 @@ object ChatModel { } fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) { - val statuses = networkStatuses.value.toMutableMap() - statuses[contact.activeConn.agentConnId] = status - networkStatuses.value = statuses + networkStatuses[contact.activeConn.agentConnId] = status } fun contactNetworkStatus(contact: Contact): NetworkStatus = - networkStatuses.value[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown() + networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown() fun addTerminalItem(item: TerminalItem) { - if (terminalItems.value.size >= 500) { - terminalItems.value = terminalItems.value.takeLast(499) + item - } else { - terminalItems.value += item + if (terminalItems.size >= 500) { + terminalItems.removeAt(0) } + terminalItems.add(item) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 16f8db0bc6..53f7c66ea5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1678,11 +1678,9 @@ object ChatController { } private fun updateContactsStatus(contactRefs: List, status: NetworkStatus) { - val statuses = chatModel.networkStatuses.value.toMutableMap() for (c in contactRefs) { - statuses[c.agentConnId] = status + chatModel.networkStatuses[c.agentConnId] = status } - chatModel.networkStatuses.value = statuses } private fun processContactSubError(contact: Contact, chatError: ChatError) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index b9f60f9cf0..faf97b4854 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -34,7 +34,7 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) { close() }) TerminalLayout( - chatModel.terminalItems, + remember { chatModel.terminalItems }, composeState, sendCommand = { sendCommand(chatModel, composeState) }, close @@ -62,7 +62,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState>, + terminalItems: List, composeState: MutableState, sendCommand: () -> Unit, close: () -> Unit @@ -115,12 +115,12 @@ fun TerminalLayout( private var lazyListState = 0 to 0 @Composable -fun TerminalLog(terminalItems: MutableState>) { +fun TerminalLog(terminalItems: List) { val listState = rememberLazyListState(lazyListState.first, lazyListState.second) DisposableEffect(Unit) { onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } } - val reversedTerminalItems by remember { derivedStateOf { terminalItems.value.reversed().toList() } } + val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } } val clipboard = LocalClipboardManager.current LazyColumn(state = listState, reverseLayout = true) { items(reversedTerminalItems) { item -> @@ -152,7 +152,7 @@ fun TerminalLog(terminalItems: MutableState>) { fun PreviewTerminalLayout() { SimpleXTheme { TerminalLayout( - terminalItems = remember { mutableStateOf(TerminalItem.sampleData) }, + terminalItems = TerminalItem.sampleData, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, sendCommand = {}, close = {} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 1d9f86eddc..85ab5c2ea3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -55,7 +55,7 @@ fun ChatInfoView( val connStats = remember { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null && currentUser != null) { - val contactNetworkStatus = remember(chatModel.networkStatuses.value) { + val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) { mutableStateOf(chatModel.contactNetworkStatus(contact)) } val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) } diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index b8168f4176..07e2de4eaf 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -32,4 +32,4 @@ desktop.version_name=1.0.1 kotlin.version=1.8.20 gradle.plugin.version=7.4.2 -compose.version=1.4.0 +compose.version=1.4.3 From 061125ab6366bb47fc77cd934c09c9a5a764a390 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 3 Aug 2023 00:30:24 +0300 Subject: [PATCH 2/8] desktop: AppImage support (#2839) --- scripts/desktop/make-appimage-linux.sh | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100755 scripts/desktop/make-appimage-linux.sh diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh new file mode 100755 index 0000000000..bf00df439d --- /dev/null +++ b/scripts/desktop/make-appimage-linux.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -e + +function readlink() { + echo "$(cd "$(dirname "$1")"; pwd -P)" +} + +root_dir="$(dirname "$(dirname "$(readlink "$0")")")" +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) + +cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/*/deps + +./gradlew createDistributable +rm common/src/commonMain/cpp/desktop/libs/*/deps/`basename $libcrypto_path` +rm desktop/src/jvmMain/resources/libs/*/`basename $libcrypto_path` + +rm -rf $release_app_dir/AppDir 2>/dev/null +mkdir -p $release_app_dir/AppDir/usr + +cd $release_app_dir/AppDir +cp -r ../*imple*/{bin,lib} usr +cp usr/lib/simplex.png . + +# For https://github.com/TheAssassin/AppImageLauncher to be able to show the icon +mkdir -p usr/share/icons +cp usr/lib/simplex.png usr/share/icons + +ln -s usr/bin/*imple* AppRun +cp $multiplatform_dir/desktop/src/jvmMain/resources/distribute/*imple*.desktop . +sed -i 's|Exec=.*|Exec=simplex|g' *imple*.desktop +sed -i 's|Icon=.*|Icon=simplex|g' *imple*.desktop + +if [ ! -f ../appimagetool-x86_64.AppImage ]; then + wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage + chmod +x ../appimagetool-x86_64.AppImage +fi +../appimagetool-x86_64.AppImage . + +mv *imple*.AppImage ../../ From 105a6afb4be0b58cefa310e6ff87b61606669b96 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 3 Aug 2023 00:45:58 +0300 Subject: [PATCH 3/8] ci: Github Action to build desktop app binaries (#2814) * desktop: Github Action for desktop * only on tag * AppImage support --- .github/workflows/build.yml | 79 +++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2c53bbe35..4eef14cf66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,15 +52,19 @@ jobs: - os: ubuntu-20.04 cache_path: ~/.cabal/store asset_name: simplex-chat-ubuntu-20_04-x86-64 + desktop_asset_name: simplex-desktop-ubuntu-20_04-x86_64.deb - os: ubuntu-22.04 cache_path: ~/.cabal/store asset_name: simplex-chat-ubuntu-22_04-x86-64 + desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb - os: macos-latest cache_path: ~/.cabal/store asset_name: simplex-chat-macos-x86-64 + desktop_asset_name: simplex-desktop-macos-x86_64.dmg - os: windows-latest cache_path: C:/cabal asset_name: simplex-chat-windows-x86-64 + desktop_asset_name: simplex-desktop-windows-x86_64.msi steps: - name: Configure pagefile (Windows) if: matrix.os == 'windows-latest' @@ -99,6 +103,10 @@ jobs: echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local + - name: Install AppImage dependencies + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-22.04' + run: sudo apt install -y desktop-file-utils + - name: Install pkg-config for Mac if: matrix.os == 'macos-latest' run: brew install pkg-config @@ -111,23 +119,86 @@ jobs: echo "package direct-sqlcipher" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local - - name: Unix build - id: unix_build + - name: Unix build CLI + id: unix_cli_build if: matrix.os != 'windows-latest' shell: bash run: | cabal build --enable-tests echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)" - - name: Unix upload binary to release + - name: Unix upload CLI binary to release if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest' uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.unix_build.outputs.bin_path }} + file: ${{ steps.unix_cli_build.outputs.bin_path }} asset_name: ${{ matrix.asset_name }} tag: ${{ github.ref }} + - name: Setup Java + if: startsWith(github.ref, 'refs/tags/v') + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: '17' + cache: 'gradle' + + - name: Linux build desktop + id: linux_desktop_build + if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') + shell: bash + run: | + scripts/desktop/build-lib-linux.sh + cd apps/multiplatform + ./gradlew packageDeb + echo "::set-output name=package_path::$(echo $PWD/release/main/deb/simplex_*_amd64.deb)" + + - name: Linux make AppImage + id: linux_appimage_build + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-22.04' + shell: bash + run: | + scripts/desktop/make-appimage-linux.sh + echo "::set-output name=appimage_path::$(echo $PWD/apps/multiplatform/release/main/*imple*.AppImage)" + + - name: Mac build desktop + id: mac_desktop_build + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' + shell: bash + run: | + scripts/desktop/build-lib-mac.sh + cd apps/multiplatform + ./gradlew packageDmg + echo "::set-output name=package_path::$(echo $PWD/release/main/dmg/simplex-*.dmg)" + + - name: Linux upload desktop package to release + if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ steps.linux_desktop_build.outputs.package_path }} + asset_name: ${{ matrix.desktop_asset_name }} + tag: ${{ github.ref }} + + - name: Linux upload AppImage to release + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-22.04' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ steps.linux_appimage_build.outputs.appimage_path }} + asset_name: simplex-desktop-x86_64.AppImage + tag: ${{ github.ref }} + + - name: Mac upload desktop package to release + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ steps.mac_desktop_build.outputs.package_path }} + asset_name: ${{ matrix.desktop_asset_name }} + tag: ${{ github.ref }} + - name: Unix test if: matrix.os != 'windows-latest' timeout-minutes: 30 From 760282cdfde74427a1fe1821b40bf0aaded8d554 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:32:01 +0300 Subject: [PATCH 4/8] desktop: edit previous message with Up arrow (#2841) --- .../simplex/common/platform/PlatformTextField.android.kt | 1 + .../chat/simplex/common/platform/PlatformTextField.kt | 1 + .../kotlin/chat/simplex/common/views/TerminalView.kt | 1 + .../kotlin/chat/simplex/common/views/chat/ComposeView.kt | 9 +++++++++ .../kotlin/chat/simplex/common/views/chat/SendMsgView.kt | 6 +++++- .../simplex/common/platform/PlatformTextField.desktop.kt | 7 ++++++- 6 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt index e9ff5687d9..ad07c6a33c 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -48,6 +48,7 @@ actual fun PlatformTextField( showDeleteTextButton: MutableState, userIsObserver: Boolean, onMessageChange: (String) -> Unit, + onUpArrow: () -> Unit, onDone: () -> Unit, ) { val cs = composeState.value diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt index 19c0fe01b3..4a8a2e204f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt @@ -12,5 +12,6 @@ expect fun PlatformTextField( showDeleteTextButton: MutableState, userIsObserver: Boolean, onMessageChange: (String) -> Unit, + onUpArrow: () -> Unit, onDone: () -> Unit, ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index faf97b4854..e8af0e71a9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -93,6 +93,7 @@ fun TerminalLayout( sendMessage = { sendCommand() }, sendLiveMessage = null, updateLiveMessage = null, + editPrevMessage = {}, onMessageChange = ::onMessageChange, textStyle = textStyle ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 143815ddc2..d4c84624af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -556,6 +556,14 @@ fun ComposeView( } } + fun editPrevMessage() { + if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return + val lastEditable = chatModel.chatItems.findLast { it.meta.editable } + if (lastEditable != null) { + composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews) + } + } + @Composable fun previewView() { when (val preview = composeState.value.preview) { @@ -754,6 +762,7 @@ fun ComposeView( composeState.value = composeState.value.copy(liveMessage = null) chatModel.removeLiveDummy() }, + editPrevMessage = ::editPrevMessage, onMessageChange = ::onMessageChange, textStyle = textStyle ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index a9ca677df9..4d9e636ad8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -48,6 +48,7 @@ fun SendMsgView( sendLiveMessage: (suspend () -> Unit)? = null, updateLiveMessage: (suspend () -> Unit)? = null, cancelLiveMessage: (() -> Unit)? = null, + editPrevMessage: () -> Unit, onMessageChange: (String) -> Unit, textStyle: MutableState ) { @@ -67,7 +68,7 @@ fun SendMsgView( val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) val showDeleteTextButton = rememberSaveable { mutableStateOf(false) } - PlatformTextField(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange) { + PlatformTextField(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) { sendMessage(null) } // Disable clicks on text field @@ -593,6 +594,7 @@ fun PreviewSendMsgView() { allowVoiceToContact = {}, timedMessageAllowed = false, sendMessage = {}, + editPrevMessage = {}, onMessageChange = { _ -> }, textStyle = textStyle ) @@ -623,6 +625,7 @@ fun PreviewSendMsgViewEditing() { allowVoiceToContact = {}, timedMessageAllowed = false, sendMessage = {}, + editPrevMessage = {}, onMessageChange = { _ -> }, textStyle = textStyle ) @@ -653,6 +656,7 @@ fun PreviewSendMsgViewInProgress() { allowVoiceToContact = {}, timedMessageAllowed = false, sendMessage = {}, + editPrevMessage = {}, onMessageChange = { _ -> }, textStyle = textStyle ) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index a8278f5331..eef710711d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -37,6 +37,7 @@ actual fun PlatformTextField( showDeleteTextButton: MutableState, userIsObserver: Boolean, onMessageChange: (String) -> Unit, + onUpArrow: () -> Unit, onDone: () -> Unit, ) { val cs = composeState.value @@ -83,7 +84,11 @@ actual fun PlatformTextField( onDone() } true - } else false + } else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.isEmpty()) { + onUpArrow() + true + } + else false }, cursorBrush = SolidColor(MaterialTheme.colors.secondary), decorationBox = { innerTextField -> From 80a77a1104204678ae7474536cdabf3ff906e870 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:25:56 +0400 Subject: [PATCH 5/8] ios: change logic of not showing alerts on file auto-receive - only don't show on file cancelled errors (#2833) --- apps/ios/Shared/Model/SimpleXAPI.swift | 20 +++++++++----------- apps/ios/SimpleXChat/APITypes.swift | 8 ++++++++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 12cab783a1..c99f176b78 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -822,21 +822,19 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil, auto: Bool = false) asyn } } else if let networkErrorAlert = networkErrorAlert(r) { logger.error("apiReceiveFile network error: \(String(describing: r))") - if !auto { - am.showAlert(networkErrorAlert) - } + am.showAlert(networkErrorAlert) } else { - switch r { - case .chatCmdError(_, .error(.fileAlreadyReceiving)): + switch chatError(r) { + case .fileCancelled: + logger.debug("apiReceiveFile ignoring fileCancelled error") + case .fileAlreadyReceiving: logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error") default: logger.error("apiReceiveFile error: \(String(describing: r))") - if !auto { - am.showAlertMsg( - title: "Error receiving file", - message: "Error: \(String(describing: r))" - ) - } + am.showAlertMsg( + title: "Error receiving file", + message: "Error: \(String(describing: r))" + ) } } return nil diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 4e37c368ce..a33f6496d7 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -821,6 +821,14 @@ public enum ChatResponse: Decodable, Error { } } +public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? { + switch chatResponse { + case let .chatCmdError(_, .error(error)): return error + case let .chatError(_, .error(error)): return error + default: return nil + } +} + struct NewUser: Encodable { var profile: Profile? var sameServers: Bool From 8bf830ced9fbb813b17a3aa638f4e737581bdbe4 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:26:23 +0400 Subject: [PATCH 6/8] android: don't show file cancelled error alerts for automatically received files (#2818) --- .../chat/simplex/common/model/SimpleXAPI.kt | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 53f7c66ea5..57a9077bbc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1080,23 +1080,26 @@ object ChatController { return false } - suspend fun apiReceiveFile(fileId: Long, inline: Boolean? = null): AChatItem? { + suspend fun apiReceiveFile(fileId: Long, inline: Boolean? = null, auto: Boolean = false): AChatItem? { val r = sendCmd(CC.ReceiveFile(fileId, inline)) return when (r) { is CR.RcvFileAccepted -> r.chatItem is CR.RcvFileAcceptedSndCancelled -> { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.cannot_receive_file), - generalGetString(MR.strings.sender_cancelled_file_transfer) - ) + Log.d(TAG, "apiReceiveFile error: sender cancelled file transfer") + if (!auto) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.cannot_receive_file), + generalGetString(MR.strings.sender_cancelled_file_transfer) + ) + } null } + else -> { if (!(networkErrorAlert(r))) { - if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat - && r.chatError.errorType is ChatErrorType.FileAlreadyReceiving - ) { - Log.d(TAG, "apiReceiveFile ignoring FileAlreadyReceiving error") + val maybeChatError = chatError(r) + if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) { + Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error") } else { apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r) } @@ -1418,7 +1421,7 @@ object ChatController { ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { - withApi { receiveFile(r.user, file.fileId) } + withApi { receiveFile(r.user, file.fileId, auto = true) } } if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id)) { ntfManager.notifyMessageReceived(r.user, cInfo, cItem) @@ -1652,8 +1655,8 @@ object ChatController { } } - suspend fun receiveFile(user: User, fileId: Long) { - val chatItem = apiReceiveFile(fileId) + suspend fun receiveFile(user: User, fileId: Long, auto: Boolean = false) { + val chatItem = apiReceiveFile(fileId, auto = auto) if (chatItem != null) { chatItemSimpleUpdate(user, chatItem) } @@ -3601,6 +3604,14 @@ sealed class CR { private fun withUser(u: User?, s: String): String = if (u != null) "userId: ${u.userId}\n$s" else s } +fun chatError(r: CR): ChatErrorType? { + return ( + if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat) r.chatError.errorType + else if (r is CR.ChatRespError && r.chatError is ChatError.ChatErrorChat) r.chatError.errorType + else null + ) +} + abstract class TerminalItem { abstract val id: Long val date: Instant = Clock.System.now() From b23b109b00f9103d6f5d1696c5a9d50bde2e8817 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:04:26 +0300 Subject: [PATCH 7/8] desktop: fixed shift+enter (#2842) --- .../common/platform/PlatformTextField.desktop.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index eef710711d..36feb1abdf 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -16,8 +16,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.* -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.TextFieldValue @@ -29,6 +28,7 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.delay import kotlin.math.min +import kotlin.text.substring @Composable actual fun PlatformTextField( @@ -74,10 +74,12 @@ actual fun PlatformTextField( .onPreviewKeyEvent { if (it.key == Key.Enter && it.type == KeyEventType.KeyDown) { if (it.isShiftPressed) { - val newText = textFieldValue.text + "\n" + val start = if (minOf(textFieldValue.selection.min) == 0) "" else textFieldValue.text.substring(0 until textFieldValue.selection.min) + val newText = start + "\n" + + textFieldValue.text.substring(textFieldValue.selection.max, textFieldValue.text.length) textFieldValueState = textFieldValue.copy( text = newText, - selection = TextRange(newText.length, newText.length) + selection = TextRange(textFieldValue.selection.min + 1) ) onMessageChange(newText) } else if (cs.message.isNotEmpty()) { From 4e27a4ea4f53d4d8f38ff411ea5bdad4fcd23e49 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 3 Aug 2023 17:41:23 +0400 Subject: [PATCH 8/8] core: rework incognito mode - set per connection (#2838) --- src/Simplex/Chat.hs | 83 +++++++------ src/Simplex/Chat/Controller.hs | 23 ++-- src/Simplex/Chat/Help.hs | 42 +++++-- src/Simplex/Chat/Store/Direct.hs | 33 ++++- src/Simplex/Chat/Store/Profiles.hs | 7 +- src/Simplex/Chat/Store/Shared.hs | 4 +- src/Simplex/Chat/Types.hs | 6 +- src/Simplex/Chat/View.hs | 10 +- tests/ChatTests/Groups.hs | 11 +- tests/ChatTests/Profiles.hs | 193 +++++++++++++++++++++++------ 10 files changed, 292 insertions(+), 120 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 79745f198b..aa896cb357 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -193,7 +193,6 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen rcvFiles <- newTVarIO M.empty currentCalls <- atomically TM.empty filesFolder <- newTVarIO optFilesFolder - incognitoMode <- newTVarIO False chatStoreChanged <- newTVarIO False expireCIThreads <- newTVarIO M.empty expireCIFlags <- newTVarIO M.empty @@ -202,7 +201,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen showLiveItems <- newTVarIO False userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg tempDirectory <- newTVarIO tempDir - pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, incognitoMode, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile} + pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile} where configServers :: DefaultAgentServers configServers = @@ -473,9 +472,6 @@ processChatCommand = \case APISetXFTPConfig cfg -> do asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg) ok_ - SetIncognito onOff -> do - asks incognitoMode >>= atomically . (`writeTVar` onOff) - ok_ APIExportArchive cfg -> checkChatStopped $ exportArchive cfg >> ok_ ExportArchive -> do ts <- liftIO getCurrentTime @@ -930,10 +926,9 @@ processChatCommand = \case pure $ CRChatCleared user (AChatInfo SCTGroup $ GroupChat gInfo) CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - APIAcceptContact connReqId -> withUser $ \_ -> withChatLock "acceptContact" $ do + APIAcceptContact incognito connReqId -> withUser $ \_ -> withChatLock "acceptContact" $ do (user, cReq) <- withStore $ \db -> getContactRequest' db connReqId -- [incognito] generate profile to send, create connection with incognito profile - incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing ct <- acceptContactRequest user cReq incognitoProfile pure $ CRAcceptingContactRequest user ct @@ -1239,32 +1234,45 @@ processChatCommand = \case EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId ChatHelp section -> pure $ CRChatHelp section Welcome -> withUser $ pure . CRWelcome - APIAddContact userId -> withUserId userId $ \user -> withChatLock "addContact" . procCmd $ do + APIAddContact userId incognito -> withUserId userId $ \user -> withChatLock "addContact" . procCmd $ do -- [incognito] generate profile for connection - incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile toView $ CRNewContactConnection user conn - pure $ CRInvitation user cReq - AddContact -> withUser $ \User {userId} -> - processChatCommand $ APIAddContact userId - APIConnect userId (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do + pure $ CRInvitation user cReq conn + AddContact incognito -> withUser $ \User {userId} -> + processChatCommand $ APIAddContact userId incognito + APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do + conn'_ <- withStore $ \db -> do + conn@PendingContactConnection {pccConnStatus, customUserProfileId} <- getPendingContactConnection db userId connId + case (pccConnStatus, customUserProfileId, incognito) of + (ConnNew, Nothing, True) -> liftIO $ do + incognitoProfile <- generateRandomProfile + pId <- createIncognitoProfile db user incognitoProfile + Just <$> updatePCCIncognito db user conn (Just pId) + (ConnNew, Just pId, False) -> liftIO $ do + deletePCCIncognitoProfile db user pId + Just <$> updatePCCIncognito db user conn Nothing + _ -> pure Nothing + case conn'_ of + Just conn' -> pure $ CRConnectionIncognitoUpdated user conn' + Nothing -> throwChatError CEConnectionIncognitoChangeProhibited + APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do -- [incognito] generate profile to send - incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = userProfileToSend user incognitoProfile Nothing connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq . directMessage $ XInfo profileToSend conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined $ incognitoProfile $> profileToSend toView $ CRNewContactConnection user conn pure $ CRSentConfirmation user - APIConnect userId (Just (ACR SCMContact cReq)) -> withUserId userId (`connectViaContact` cReq) - APIConnect _ Nothing -> throwChatError CEInvalidConnReq - Connect cReqUri -> withUser $ \User {userId} -> - processChatCommand $ APIConnect userId cReqUri - ConnectSimplex -> withUser $ \user -> + APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq + APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq + Connect incognito cReqUri -> withUser $ \User {userId} -> + processChatCommand $ APIConnect userId incognito cReqUri + ConnectSimplex incognito -> withUser $ \user -> -- [incognito] generate profile to send - connectViaContact user adminContactReq + connectViaContact user incognito adminContactReq DeleteContact cName -> withContactName cName $ APIDeleteChat . ChatRef CTDirect ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect APIListContacts userId -> withUserId userId $ \user -> @@ -1308,9 +1316,9 @@ processChatCommand = \case pure $ CRUserContactLinkUpdated user contactLink AddressAutoAccept autoAccept_ -> withUser $ \User {userId} -> processChatCommand $ APIAddressAutoAccept userId autoAccept_ - AcceptContact cName -> withUser $ \User {userId} -> do + AcceptContact incognito cName -> withUser $ \User {userId} -> do connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName - processChatCommand $ APIAcceptContact connReqId + processChatCommand $ APIAcceptContact incognito connReqId RejectContact cName -> withUser $ \User {userId} -> do connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName processChatCommand $ APIRejectContact connReqId @@ -1754,8 +1762,8 @@ processChatCommand = \case CTDirect -> withStore $ \db -> getDirectChatItemIdByText' db user cId msg CTGroup -> withStore $ \db -> getGroupChatItemIdByText' db user cId msg _ -> throwChatError $ CECommandError "not supported" - connectViaContact :: User -> ConnectionRequestUri 'CMContact -> m ChatResponse - connectViaContact user@User {userId} cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do + connectViaContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> m ChatResponse + connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq withStore' (\db -> getConnReqContactXContactId db user cReqHash) >>= \case (Just contact, _) -> pure $ CRContactAlreadyExists user contact @@ -1763,11 +1771,6 @@ processChatCommand = \case let randomXContactId = XContactId <$> drgRandomBytes 16 xContactId <- maybe randomXContactId pure xContactId_ -- [incognito] generate profile to send - -- if user makes a contact request using main profile, then turns on incognito mode and repeats the request, - -- an incognito profile will be sent even though the address holder will have user's main profile received as well; - -- we ignore this edge case as we already allow profile updates on repeat contact requests; - -- alternatively we can re-send the main profile even if incognito mode is enabled - incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = userProfileToSend user incognitoProfile Nothing connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq $ directMessage (XContact profileToSend $ Just xContactId) @@ -3436,7 +3439,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do setActive $ ActiveG g showToast ("#" <> g) $ "member " <> c <> " is connected" - probeMatchingContacts :: Contact -> Bool -> m () + probeMatchingContacts :: Contact -> IncognitoEnabled -> m () probeMatchingContacts ct connectedIncognito = do gVar <- asks idsDrg (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId ct @@ -5033,7 +5036,7 @@ chatCommandP = "/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP), "/_delete " *> (APIDeleteChat <$> chatRefP), "/_clear chat " *> (APIClearChat <$> chatRefP), - "/_accept " *> (APIAcceptContact <$> A.decimal), + "/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal), "/_reject " *> (APIRejectContact <$> A.decimal), "/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP), "/call " *> char_ '@' *> (SendCallInvitation <$> displayName <*> pure defaultCallType), @@ -5112,6 +5115,7 @@ chatCommandP = ("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups, ("/help contacts" <|> "/help contact" <|> "/hc") $> ChatHelp HSContacts, ("/help address" <|> "/ha") $> ChatHelp HSMyAddress, + "/help incognito" $> ChatHelp HSIncognito, ("/help messages" <|> "/hm") $> ChatHelp HSMessages, ("/help settings" <|> "/hs") $> ChatHelp HSSettings, ("/help db" <|> "/hd") $> ChatHelp HSDatabase, @@ -5145,10 +5149,11 @@ chatCommandP = (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> msgTextP), "/_contacts " *> (APIListContacts <$> A.decimal), "/contacts" $> ListContacts, - "/_connect " *> (APIConnect <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), - "/_connect " *> (APIAddContact <$> A.decimal), - ("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeByteString $> Nothing)), - ("/connect" <|> "/c") $> AddContact, + "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), + "/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP), + "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), + ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), + ("/connect" <|> "/c") *> (AddContact <$> incognitoP), SendMessage <$> chatNameP <* A.space <*> msgTextP, "/live " *> (SendLiveMessage <$> chatNameP <*> (A.space *> msgTextP <|> pure "")), (">@" <|> "> @") *> sendMsgQuote (AMsgDirection SMDRcv), @@ -5174,7 +5179,7 @@ chatCommandP = "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), - "/simplex" $> ConnectSimplex, + "/simplex" *> (ConnectSimplex <$> incognitoP), "/_address " *> (APICreateMyAddress <$> A.decimal), ("/address" <|> "/ad") $> CreateMyAddress, "/_delete_address " *> (APIDeleteMyAddress <$> A.decimal), @@ -5185,7 +5190,7 @@ chatCommandP = ("/profile_address " <|> "/pa ") *> (SetProfileAddress <$> onOffP), "/_auto_accept " *> (APIAddressAutoAccept <$> A.decimal <* A.space <*> autoAcceptP), "/auto_accept " *> (AddressAutoAccept <$> autoAcceptP), - ("/accept " <|> "/ac ") *> char_ '@' *> (AcceptContact <$> displayName), + ("/accept" <|> "/ac") *> (AcceptContact <$> incognitoP <* A.space <* char_ '@' <*> displayName), ("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayName), ("/markdown" <|> "/m") $> ChatHelp HSMarkdown, ("/welcome" <|> "/w") $> Welcome, @@ -5207,7 +5212,7 @@ chatCommandP = "/set disappear #" *> (SetGroupTimedMessages <$> displayName <*> (A.space *> timedTTLOnOffP)), "/set disappear @" *> (SetContactTimedMessages <$> displayName <*> optional (A.space *> timedMessagesEnabledP)), "/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))), - "/incognito " *> (SetIncognito <$> onOffP), + ("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito, ("/quit" <|> "/q" <|> "/exit") $> QuitChat, ("/version" <|> "/v") $> ShowVersion, "/debug locks" $> DebugLocks, @@ -5216,6 +5221,8 @@ chatCommandP = ] where choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput) + incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False + incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,") imageP = safeDecodeUtf8 <$> ((<>) <$> imagePrefix <*> (B64.encode <$> base64P)) chatTypeP = A.char '@' $> CTDirect <|> A.char '#' $> CTGroup <|> A.char ':' $> CTContactConnection diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 90f90fdcb6..06e2009543 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -176,7 +176,6 @@ data ChatController = ChatController currentCalls :: TMap ContactId Call, config :: ChatConfig, filesFolder :: TVar (Maybe FilePath), -- path to files folder for mobile apps, - incognitoMode :: TVar Bool, expireCIThreads :: TMap UserId (Maybe (Async ())), expireCIFlags :: TMap UserId Bool, cleanupManagerAsync :: TVar (Maybe (Async ())), @@ -187,7 +186,7 @@ data ChatController = ChatController logFilePath :: Maybe FilePath } -data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSMarkdown | HSMessages | HSSettings | HSDatabase +data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSIncognito | HSMarkdown | HSMessages | HSSettings | HSDatabase deriving (Show, Generic) instance ToJSON HelpSection where @@ -223,7 +222,6 @@ data ChatCommand | SetTempFolder FilePath | SetFilesFolder FilePath | APISetXFTPConfig (Maybe XFTPFileConfig) - | SetIncognito Bool | APIExportArchive ArchiveConfig | ExportArchive | APIImportArchive ArchiveConfig @@ -244,7 +242,7 @@ data ChatCommand | APIChatUnread ChatRef Bool | APIDeleteChat ChatRef | APIClearChat ChatRef - | APIAcceptContact Int64 + | APIAcceptContact IncognitoEnabled Int64 | APIRejectContact Int64 | APISendCallInvitation ContactId CallType | SendCallInvitation ContactName CallType @@ -322,11 +320,12 @@ data ChatCommand | EnableGroupMember GroupName ContactName | ChatHelp HelpSection | Welcome - | APIAddContact UserId - | AddContact - | APIConnect UserId (Maybe AConnectionRequestUri) - | Connect (Maybe AConnectionRequestUri) - | ConnectSimplex -- UserId (not used in UI) + | APIAddContact UserId IncognitoEnabled + | AddContact IncognitoEnabled + | APISetConnectionIncognito Int64 IncognitoEnabled + | APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri) + | Connect IncognitoEnabled (Maybe AConnectionRequestUri) + | ConnectSimplex IncognitoEnabled -- UserId (not used in UI) | DeleteContact ContactName | ClearContact ContactName | APIListContacts UserId @@ -341,7 +340,7 @@ data ChatCommand | SetProfileAddress Bool | APIAddressAutoAccept UserId (Maybe AutoAccept) | AddressAutoAccept (Maybe AutoAccept) - | AcceptContact ContactName + | AcceptContact IncognitoEnabled ContactName | RejectContact ContactName | SendMessage ChatName Text | SendLiveMessage ChatName Text @@ -467,7 +466,8 @@ data ChatResponse | CRUserProfileNoChange {user :: User} | CRUserPrivacy {user :: User, updatedUser :: User} | CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]} - | CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation} + | CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection} + | CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection} | CRSentConfirmation {user :: User} | CRSentInvitation {user :: User, customUserProfile :: Maybe Profile} | CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact} @@ -876,6 +876,7 @@ data ChatErrorType | CEServerProtocol {serverProtocol :: AProtocolType} | CEAgentCommandError {message :: String} | CEInvalidFileDescription {message :: String} + | CEConnectionIncognitoChangeProhibited | CEInternalError {message :: String} | CEException {message :: String} deriving (Show, Exception, Generic) diff --git a/src/Simplex/Chat/Help.hs b/src/Simplex/Chat/Help.hs index c83e81a9ef..c2a5720633 100644 --- a/src/Simplex/Chat/Help.hs +++ b/src/Simplex/Chat/Help.hs @@ -8,6 +8,7 @@ module Simplex.Chat.Help groupsHelpInfo, contactsHelpInfo, myAddressHelpInfo, + incognitoHelpInfo, messagesHelpInfo, markdownInfo, settingsInfo, @@ -48,7 +49,7 @@ chatWelcome user = "Welcome " <> green userName <> "!", "Thank you for installing SimpleX Chat!", "", - "Connect to SimpleX Chat lead developer for any questions - just type " <> highlight "/simplex", + "Connect to SimpleX Chat developers for any questions - just type " <> highlight "/simplex", "", "Follow our updates:", "> Reddit: https://www.reddit.com/r/SimpleXChat/", @@ -213,6 +214,26 @@ myAddressHelpInfo = "The commands may be abbreviated: " <> listHighlight ["/ad", "/da", "/sa", "/ac", "/rc"] ] +incognitoHelpInfo :: [StyledString] +incognitoHelpInfo = + map + styleMarkdown + [ markdown (colored Red) "/incognito" <> " command is deprecated, use commands below instead.", + "", + "Incognito mode protects the privacy of your main profile — you can choose to create a new random profile for each new contact.", + "It allows having many anonymous connections without any shared data between them in a single chat profile.", + "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.", + "", + green "Incognito commands:", + indent <> highlight "/connect incognito " <> " - create new invitation link using incognito profile", + indent <> highlight "/connect incognito " <> " - accept invitation using incognito profile", + indent <> highlight "/accept incognito " <> " - accept contact request using incognito profile", + indent <> highlight "/simplex incognito " <> " - connect to SimpleX Chat developers using incognito profile", + "", + "The commands may be abbreviated: " <> listHighlight ["/c i", "/c i ", "/ac i "], + "To find the profile used for an incognito connection, use " <> highlight "/info " <> "." + ] + messagesHelpInfo :: [StyledString] messagesHelpInfo = map @@ -269,7 +290,6 @@ settingsInfo = map styleMarkdown [ green "Chat settings:", - indent <> highlight "/incognito on/off " <> " - enable/disable incognito mode", indent <> highlight "/network " <> " - show / set network access options", indent <> highlight "/smp " <> " - show / set configured SMP servers", indent <> highlight "/xftp " <> " - show / set configured XFTP servers", @@ -285,12 +305,12 @@ databaseHelpInfo :: [StyledString] databaseHelpInfo = map styleMarkdown - [ green "Database export:", - indent <> highlight "/db export " <> " - create database export file that can be imported in mobile apps", - indent <> highlight "/files_folder " <> " - set files folder path to include app files in the exported archive", - "", - green "Database encryption:", - indent <> highlight "/db encrypt " <> " - encrypt chat database with key/passphrase", - indent <> highlight "/db key " <> " - change the key of the encrypted app database", - indent <> highlight "/db decrypt " <> " - decrypt chat database" - ] + [ green "Database export:", + indent <> highlight "/db export " <> " - create database export file that can be imported in mobile apps", + indent <> highlight "/files_folder " <> " - set files folder path to include app files in the exported archive", + "", + green "Database encryption:", + indent <> highlight "/db encrypt " <> " - encrypt chat database with key/passphrase", + indent <> highlight "/db key " <> " - change the key of the encrypted app database", + indent <> highlight "/db decrypt " <> " - decrypt chat database" + ] diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 944527ae48..9c18350b85 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -17,6 +17,7 @@ module Simplex.Chat.Store.Direct getPendingContactConnection, deletePendingContactConnection, createDirectConnection, + createIncognitoProfile, createConnReqConnection, getProfileById, getConnReqContactXContactId, @@ -33,6 +34,8 @@ module Simplex.Chat.Store.Direct updateContactUserPreferences, updateContactAlias, updateContactConnectionAlias, + updatePCCIncognito, + deletePCCIncognitoProfile, updateContactUsed, updateContactUnreadChat, updateGroupUnreadChat, @@ -171,6 +174,11 @@ createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile pccConnId <- insertedRowId db pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt} +createIncognitoProfile :: DB.Connection -> User -> Profile -> IO Int64 +createIncognitoProfile db User {userId} p = do + createdAt <- getCurrentTime + createIncognitoProfile_ db userId createdAt p + createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64 createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, image} = do DB.execute @@ -307,7 +315,30 @@ updateContactConnectionAlias db userId conn localAlias = do WHERE user_id = ? AND connection_id = ? |] (localAlias, updatedAt, userId, pccConnId conn) - pure (conn :: PendingContactConnection) {localAlias} + pure (conn :: PendingContactConnection) {localAlias, updatedAt} + +updatePCCIncognito :: DB.Connection -> User -> PendingContactConnection -> Maybe ProfileId -> IO PendingContactConnection +updatePCCIncognito db User {userId} conn customUserProfileId = do + updatedAt <- getCurrentTime + DB.execute + db + [sql| + UPDATE connections + SET custom_user_profile_id = ?, updated_at = ? + WHERE user_id = ? AND connection_id = ? + |] + (customUserProfileId, updatedAt, userId, pccConnId conn) + pure (conn :: PendingContactConnection) {customUserProfileId, updatedAt} + +deletePCCIncognitoProfile :: DB.Connection -> User -> ProfileId -> IO () +deletePCCIncognitoProfile db User {userId} profileId = + DB.execute + db + [sql| + DELETE FROM contact_profiles + WHERE user_id = ? AND contact_profile_id = ? AND incognito = 1 + |] + (userId, profileId) updateContactUsed :: DB.Connection -> User -> Contact -> IO () updateContactUsed db User {userId} Contact {contactId} = do diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index ddbe665d71..4577712f0e 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -397,14 +397,14 @@ data UserContactLink = UserContactLink instance ToJSON UserContactLink where toEncoding = J.genericToEncoding J.defaultOptions data AutoAccept = AutoAccept - { acceptIncognito :: Bool, + { acceptIncognito :: IncognitoEnabled, autoReply :: Maybe MsgContent } deriving (Show, Generic) instance ToJSON AutoAccept where toEncoding = J.genericToEncoding J.defaultOptions -toUserContactLink :: (ConnReqContact, Bool, Bool, Maybe MsgContent) -> UserContactLink +toUserContactLink :: (ConnReqContact, Bool, IncognitoEnabled, Maybe MsgContent) -> UserContactLink toUserContactLink (connReq, autoAccept, acceptIncognito, autoReply) = UserContactLink connReq $ if autoAccept then Just AutoAccept {acceptIncognito, autoReply} else Nothing @@ -452,9 +452,6 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply) _ -> (False, False, Nothing) - - - getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p] getProtocolServers db User {userId} = map toServerCfg diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 9e4d3c0e02..ad3116695d 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -203,7 +203,7 @@ createContact_ db userId connId Profile {displayName, fullName, image, contactLi pure $ Right (ldn, contactId, profileId) deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO () -deleteUnusedIncognitoProfileById_ db User {userId} profile_id = +deleteUnusedIncognitoProfileById_ db User {userId} profileId = DB.executeNamed db [sql| @@ -218,7 +218,7 @@ deleteUnusedIncognitoProfileById_ db User {userId} profile_id = WHERE user_id = :user_id AND member_profile_id = :profile_id LIMIT 1 ) |] - [":user_id" := userId, ":profile_id" := profile_id] + [":user_id" := userId, ":profile_id" := profileId] type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 9d9791f1a9..3790617507 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -184,7 +184,9 @@ contactConn = activeConn contactConnId :: Contact -> ConnId contactConnId = aConnId . contactConn -contactConnIncognito :: Contact -> Bool +type IncognitoEnabled = Bool + +contactConnIncognito :: Contact -> IncognitoEnabled contactConnIncognito = connIncognito . contactConn contactDirect :: Contact -> Bool @@ -595,7 +597,7 @@ memberConnId GroupMember {activeConn} = aConnId <$> activeConn groupMemberId' :: GroupMember -> GroupMemberId groupMemberId' GroupMember {groupMemberId} = groupMemberId -memberIncognito :: GroupMember -> Bool +memberIncognito :: GroupMember -> IncognitoEnabled memberIncognito GroupMember {memberProfile, memberContactProfileId} = localProfileId memberProfile /= memberContactProfileId memberSecurityCode :: GroupMember -> Maybe SecurityCode diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index d6e443401d..d1e47b1d4d 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -115,6 +115,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView HSGroups -> groupsHelpInfo HSContacts -> contactsHelpInfo HSMyAddress -> myAddressHelpInfo + HSIncognito -> incognitoHelpInfo HSMessages -> messagesHelpInfo HSMarkdown -> markdownInfo HSSettings -> settingsInfo @@ -138,7 +139,8 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRUserProfileNoChange u -> ttyUser u ["user profile did not change"] CRUserPrivacy u u' -> ttyUserPrefix u $ viewUserPrivacy u u' CRVersionInfo info _ _ -> viewVersionInfo logLevel info - CRInvitation u cReq -> ttyUser u $ viewConnReqInvitation cReq + CRInvitation u cReq _ -> ttyUser u $ viewConnReqInvitation cReq + CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c CRSentConfirmation u -> ttyUser u ["confirmation sent!"] CRSentInvitation u customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"] @@ -1148,6 +1150,11 @@ viewConnectionAliasUpdated PendingContactConnection {pccConnId, localAlias} | localAlias == "" = ["connection " <> sShow pccConnId <> " alias removed"] | otherwise = ["connection " <> sShow pccConnId <> " alias updated: " <> plain localAlias] +viewConnectionIncognitoUpdated :: PendingContactConnection -> [StyledString] +viewConnectionIncognitoUpdated PendingContactConnection {pccConnId, customUserProfileId} + | isJust customUserProfileId = ["connection " <> sShow pccConnId <> " changed to incognito"] + | otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"] + viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated Contact {localDisplayName = n, profile = LocalProfile {fullName, contactLink}} @@ -1539,6 +1546,7 @@ viewChatError logLevel = \case CECommandError e -> ["bad chat command: " <> plain e] CEAgentCommandError e -> ["agent command error: " <> plain e] CEInvalidFileDescription e -> ["invalid file description: " <> plain e] + CEConnectionIncognitoChangeProhibited -> ["incognito mode change prohibited"] CEInternalError e -> ["internal chat error: " <> plain e] CEException e -> ["exception: " <> plain e] -- e -> ["chat error: " <> sShow e] diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 6e1e761208..e4cc94d81b 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -1817,8 +1817,7 @@ testGroupLinkIncognitoMembership = -- bob connected incognito to alice alice ##> "/c" inv <- getInvitation alice - bob #$> ("/incognito on", id, "ok") - bob ##> ("/c " <> inv) + bob ##> ("/c i " <> inv) bob <## "confirmation sent!" bobIncognito <- getTermLine bob concurrentlyN_ @@ -1827,7 +1826,6 @@ testGroupLinkIncognitoMembership = bob <## "use /i alice to print out this incognito profile again", alice <## (bobIncognito <> ": contact is connected") ] - bob #$> ("/incognito off", id, "ok") -- alice creates group alice ##> "/g team" alice <## "group #team is created" @@ -1870,8 +1868,7 @@ testGroupLinkIncognitoMembership = cath #> ("@" <> bobIncognito <> " hey, I'm cath") bob ?<# "cath> hey, I'm cath" -- dan joins incognito - dan #$> ("/incognito on", id, "ok") - dan ##> ("/c " <> gLink) + dan ##> ("/c i " <> gLink) danIncognito <- getTermLine dan dan <## "connection request sent incognito!" bob <## (danIncognito <> ": accepting request to join group #team...") @@ -1898,7 +1895,6 @@ testGroupLinkIncognitoMembership = cath <## ("#team: " <> bobIncognito <> " added " <> danIncognito <> " to the group (connecting...)") cath <## ("#team: new member " <> danIncognito <> " is connected") ] - dan #$> ("/incognito off", id, "ok") bob ?#> ("@" <> danIncognito <> " hi, I'm incognito") dan ?<# (bobIncognito <> "> hi, I'm incognito") dan ?#> ("@" <> bobIncognito <> " hey, me too") @@ -2006,7 +2002,6 @@ testGroupLinkIncognitoUnusedHostContactsDeleted :: HasCallStack => FilePath -> I testGroupLinkIncognitoUnusedHostContactsDeleted = testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do - bob #$> ("/incognito on", id, "ok") bobIncognitoTeam <- createGroupBobIncognito alice bob "team" "alice" bobIncognitoClub <- createGroupBobIncognito alice bob "club" "alice_1" bobIncognitoTeam `shouldNotBe` bobIncognitoClub @@ -2036,7 +2031,7 @@ testGroupLinkIncognitoUnusedHostContactsDeleted = alice <## ("to add members use /a " <> group <> " or /create link #" <> group) alice ##> ("/create link #" <> group) gLinkTeam <- getGroupLink alice group GRMember True - bob ##> ("/c " <> gLinkTeam) + bob ##> ("/c i " <> gLinkTeam) bobIncognito <- getTermLine bob bob <## "connection request sent incognito!" alice <## (bobIncognito <> ": accepting request to join group #" <> group <> "...") diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 08d33df1d6..1229aede0e 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -27,10 +27,15 @@ chatProfileTests = do it "delete connection requests when contact link deleted" testDeleteConnectionRequests it "auto-reply message" testAutoReplyMessage it "auto-reply message in incognito" testAutoReplyMessageInIncognito - describe "incognito mode" $ do + describe "incognito" $ do it "connect incognito via invitation link" testConnectIncognitoInvitationLink it "connect incognito via contact address" testConnectIncognitoContactAddress it "accept contact request incognito" testAcceptContactRequestIncognito + it "set connection incognito" testSetConnectionIncognito + it "reset connection incognito" testResetConnectionIncognito + it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation + it "connection incognito unchanged errors" testConnectionIncognitoUnchangedErrors + it "set, reset, set connection incognito" testSetResetSetConnectionIncognito it "join group incognito" testJoinGroupIncognito it "can't invite contact to whom user connected incognito to a group" testCantInviteContactIncognito it "can't see global preferences update" testCantSeeGlobalPrefsUpdateIncognito @@ -489,11 +494,9 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $ testConnectIncognitoInvitationLink :: HasCallStack => FilePath -> IO () testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do - alice #$> ("/incognito on", id, "ok") - bob #$> ("/incognito on", id, "ok") - alice ##> "/c" + alice ##> "/connect incognito" inv <- getInvitation alice - bob ##> ("/c " <> inv) + bob ##> ("/connect incognito " <> inv) bob <## "confirmation sent!" bobIncognito <- getTermLine bob aliceIncognito <- getTermLine alice @@ -505,9 +508,6 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi alice <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> aliceIncognito) alice <## ("use /i " <> bobIncognito <> " to print out this incognito profile again") ] - -- after turning incognito mode off conversation is incognito - alice #$> ("/incognito off", id, "ok") - bob #$> ("/incognito off", id, "ok") alice ?#> ("@" <> bobIncognito <> " psst, I'm incognito") bob ?<# (aliceIncognito <> "> psst, I'm incognito") bob ?#> ("@" <> aliceIncognito <> " me too") @@ -569,8 +569,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/ad" cLink <- getContactLink alice True - bob #$> ("/incognito on", id, "ok") - bob ##> ("/c " <> cLink) + bob ##> ("/c i " <> cLink) bobIncognito <- getTermLine bob bob <## "connection request sent incognito!" alice <## (bobIncognito <> " wants to connect to you!") @@ -585,9 +584,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ bob <## "use /i alice to print out this incognito profile again", alice <## (bobIncognito <> ": contact is connected") ] - -- after turning incognito mode off conversation is incognito - alice #$> ("/incognito off", id, "ok") - bob #$> ("/incognito off", id, "ok") + -- conversation is incognito alice #> ("@" <> bobIncognito <> " who are you?") bob ?<# "alice> who are you?" bob ?#> "@alice I'm Batman" @@ -605,39 +602,162 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ bob `hasContactProfiles` ["bob"] testAcceptContactRequestIncognito :: HasCallStack => FilePath -> IO () -testAcceptContactRequestIncognito = testChat2 aliceProfile bobProfile $ - \alice bob -> do +testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do alice ##> "/ad" cLink <- getContactLink alice True bob ##> ("/c " <> cLink) alice <#? bob - alice #$> ("/incognito on", id, "ok") - alice ##> "/ac bob" + alice ##> "/accept incognito bob" alice <## "bob (Bob): accepting contact request..." - aliceIncognito <- getTermLine alice + aliceIncognitoBob <- getTermLine alice concurrentlyN_ - [ bob <## (aliceIncognito <> ": contact is connected"), + [ bob <## (aliceIncognitoBob <> ": contact is connected"), do - alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognitoBob) alice <## "use /i bob to print out this incognito profile again" ] - -- after turning incognito mode off conversation is incognito - alice #$> ("/incognito off", id, "ok") - bob #$> ("/incognito off", id, "ok") + -- conversation is incognito alice ?#> "@bob my profile is totally inconspicuous" - bob <# (aliceIncognito <> "> my profile is totally inconspicuous") - bob #> ("@" <> aliceIncognito <> " I know!") + bob <# (aliceIncognitoBob <> "> my profile is totally inconspicuous") + bob #> ("@" <> aliceIncognitoBob <> " I know!") alice ?<# "bob> I know!" -- list contacts alice ##> "/contacts" alice <## "i bob (Bob)" - alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito] + alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognitoBob] -- delete contact, incognito profile is deleted alice ##> "/d bob" alice <## "bob: contact is deleted" alice ##> "/contacts" (alice ("/c " <> cLink) + alice <#? cath + alice ##> "/_accept incognito=on 1" + alice <## "cath (Catherine): accepting contact request..." + aliceIncognitoCath <- getTermLine alice + concurrentlyN_ + [ cath <## (aliceIncognitoCath <> ": contact is connected"), + do + alice <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> aliceIncognitoCath) + alice <## "use /i cath to print out this incognito profile again" + ] + alice `hasContactProfiles` ["alice", "cath", T.pack aliceIncognitoCath] + cath `hasContactProfiles` ["cath", T.pack aliceIncognitoCath] + +testSetConnectionIncognito :: HasCallStack => FilePath -> IO () +testSetConnectionIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/connect" + inv <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + aliceIncognito <- getTermLine alice + concurrentlyN_ + [ bob <## (aliceIncognito <> ": contact is connected"), + do + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## ("use /i bob to print out this incognito profile again") + ] + alice ?#> ("@bob hi") + bob <# (aliceIncognito <> "> hi") + bob #> ("@" <> aliceIncognito <> " hey") + alice ?<# ("bob> hey") + alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito] + bob `hasContactProfiles` ["bob", T.pack aliceIncognito] + +testResetConnectionIncognito :: HasCallStack => FilePath -> IO () +testResetConnectionIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/_connect 1 incognito=on" + inv <- getInvitation alice + alice ##> "/_set incognito :1 off" + alice <## "connection 1 changed to non incognito" + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + alice `hasContactProfiles` ["alice", "bob"] + bob `hasContactProfiles` ["alice", "bob"] + +testSetConnectionIncognitoProhibitedDuringNegotiation :: HasCallStack => FilePath -> IO () +testSetConnectionIncognitoProhibitedDuringNegotiation tmp = do + inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do + threadDelay 250000 + alice ##> "/connect" + getInvitation alice + withNewTestChat tmp "bob" bobProfile $ \bob -> do + threadDelay 250000 + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + withTestChat tmp "alice" $ \alice -> do + threadDelay 250000 + alice ##> "/_set incognito :1 on" + alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}" + withTestChat tmp "bob" $ \bob -> do + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + alice `hasContactProfiles` ["alice", "bob"] + bob `hasContactProfiles` ["alice", "bob"] + +testConnectionIncognitoUnchangedErrors :: HasCallStack => FilePath -> IO () +testConnectionIncognitoUnchangedErrors = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/connect" + inv <- getInvitation alice + alice ##> "/_set incognito :1 off" + alice <## "incognito mode change prohibited" + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + alice ##> "/_set incognito :1 on" + alice <## "incognito mode change prohibited" + alice ##> "/_set incognito :1 off" + alice <## "connection 1 changed to non incognito" + alice ##> "/_set incognito :1 off" + alice <## "incognito mode change prohibited" + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + alice `hasContactProfiles` ["alice", "bob"] + bob `hasContactProfiles` ["alice", "bob"] + +testSetResetSetConnectionIncognito :: HasCallStack => FilePath -> IO () +testSetResetSetConnectionIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/_connect 1 incognito=off" + inv <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + alice ##> "/_set incognito :1 off" + alice <## "connection 1 changed to non incognito" + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + bob ##> ("/_connect 1 incognito=off " <> inv) + bob <## "confirmation sent!" + aliceIncognito <- getTermLine alice + concurrentlyN_ + [ bob <## (aliceIncognito <> ": contact is connected"), + do + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## ("use /i bob to print out this incognito profile again") + ] + alice ?#> ("@bob hi") + bob <# (aliceIncognito <> "> hi") + bob #> ("@" <> aliceIncognito <> " hey") + alice ?<# ("bob> hey") + alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito] + bob `hasContactProfiles` ["bob", T.pack aliceIncognito] testJoinGroupIncognito :: HasCallStack => FilePath -> IO () testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfile $ @@ -651,8 +771,7 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil -- cath connected incognito to alice alice ##> "/c" inv <- getInvitation alice - cath #$> ("/incognito on", id, "ok") - cath ##> ("/c " <> inv) + cath ##> ("/c i " <> inv) cath <## "confirmation sent!" cathIncognito <- getTermLine cath concurrentlyN_ @@ -685,10 +804,8 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil cath <## "#secret_club: alice invites you to join the group as admin" cath <## ("use /j secret_club to join incognito as " <> cathIncognito) ] - -- cath uses the same incognito profile when joining group, disabling incognito mode doesn't affect it - cath #$> ("/incognito off", id, "ok") + -- cath uses the same incognito profile when joining group, cath and bob don't merge contacts cath ##> "/j secret_club" - -- cath and bob don't merge contacts concurrentlyN_ [ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"), do @@ -834,8 +951,7 @@ testCantInviteContactIncognito :: HasCallStack => FilePath -> IO () testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do -- alice connected incognito to bob - alice #$> ("/incognito on", id, "ok") - alice ##> "/c" + alice ##> "/c i" inv <- getInvitation alice bob ##> ("/c " <> inv) bob <## "confirmation sent!" @@ -847,7 +963,6 @@ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ alice <## "use /i bob to print out this incognito profile again" ] -- alice creates group non incognito - alice #$> ("/incognito off", id, "ok") alice ##> "/g club" alice <## "group #club is created" alice <## "to add members use /a club or /create link #club" @@ -859,10 +974,8 @@ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ testCantSeeGlobalPrefsUpdateIncognito :: HasCallStack => FilePath -> IO () testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do - alice #$> ("/incognito on", id, "ok") - alice ##> "/c" + alice ##> "/c i" invIncognito <- getInvitation alice - alice #$> ("/incognito off", id, "ok") alice ##> "/c" inv <- getInvitation alice bob ##> ("/c " <> invIncognito) @@ -915,8 +1028,7 @@ testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobPr -- bob connects incognito to alice alice ##> "/c" inv <- getInvitation alice - bob #$> ("/incognito on", id, "ok") - bob ##> ("/c " <> inv) + bob ##> ("/c i " <> inv) bob <## "confirmation sent!" bobIncognito <- getTermLine bob concurrentlyN_ @@ -967,8 +1079,7 @@ testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobPr -- bob connects incognito to alice alice ##> "/c" inv <- getInvitation alice - bob #$> ("/incognito on", id, "ok") - bob ##> ("/c " <> inv) + bob ##> ("/c i " <> inv) bob <## "confirmation sent!" bobIncognito <- getTermLine bob concurrentlyN_