mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-17 23:04:26 +00:00
Merge branch 'master' into master-ios
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ actual fun PlatformTextField(
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
onMessageChange: (String) -> Unit,
|
||||
onUpArrow: () -> Unit,
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
val cs = composeState.value
|
||||
|
||||
+7
-10
@@ -49,14 +49,14 @@ object ChatModel {
|
||||
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
|
||||
val chats = mutableStateListOf<Chat>()
|
||||
// map of connections network statuses, key is agent connection id
|
||||
val networkStatuses = mutableStateOf<Map<String, NetworkStatus>>(mapOf())
|
||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||
|
||||
// current chat
|
||||
val chatId = mutableStateOf<String?>(null)
|
||||
val chatItems = mutableStateListOf<ChatItem>()
|
||||
val groupMembers = mutableStateListOf<GroupMember>()
|
||||
|
||||
val terminalItems = mutableStateOf(emptyList<TerminalItem>())
|
||||
val terminalItems = mutableStateListOf<TerminalItem>()
|
||||
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
|
||||
// Allows to temporary save servers that are being edited on multiple screens
|
||||
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-15
@@ -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)
|
||||
}
|
||||
@@ -1678,11 +1681,9 @@ object ChatController {
|
||||
}
|
||||
|
||||
private fun updateContactsStatus(contactRefs: List<ContactRef>, 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) {
|
||||
@@ -3603,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()
|
||||
|
||||
+1
@@ -12,5 +12,6 @@ expect fun PlatformTextField(
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
onMessageChange: (String) -> Unit,
|
||||
onUpArrow: () -> Unit,
|
||||
onDone: () -> Unit,
|
||||
)
|
||||
|
||||
+6
-5
@@ -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<Compose
|
||||
|
||||
@Composable
|
||||
fun TerminalLayout(
|
||||
terminalItems: MutableState<List<TerminalItem>>,
|
||||
terminalItems: List<TerminalItem>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
sendCommand: () -> Unit,
|
||||
close: () -> Unit
|
||||
@@ -93,6 +93,7 @@ fun TerminalLayout(
|
||||
sendMessage = { sendCommand() },
|
||||
sendLiveMessage = null,
|
||||
updateLiveMessage = null,
|
||||
editPrevMessage = {},
|
||||
onMessageChange = ::onMessageChange,
|
||||
textStyle = textStyle
|
||||
)
|
||||
@@ -115,12 +116,12 @@ fun TerminalLayout(
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
fun TerminalLog(terminalItems: MutableState<List<TerminalItem>>) {
|
||||
fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
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 +153,7 @@ fun TerminalLog(terminalItems: MutableState<List<TerminalItem>>) {
|
||||
fun PreviewTerminalLayout() {
|
||||
SimpleXTheme {
|
||||
TerminalLayout(
|
||||
terminalItems = remember { mutableStateOf(TerminalItem.sampleData) },
|
||||
terminalItems = TerminalItem.sampleData,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
|
||||
sendCommand = {},
|
||||
close = {}
|
||||
|
||||
+1
-1
@@ -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)) }
|
||||
|
||||
+9
@@ -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
|
||||
)
|
||||
|
||||
+5
-1
@@ -48,6 +48,7 @@ fun SendMsgView(
|
||||
sendLiveMessage: (suspend () -> Unit)? = null,
|
||||
updateLiveMessage: (suspend () -> Unit)? = null,
|
||||
cancelLiveMessage: (() -> Unit)? = null,
|
||||
editPrevMessage: () -> Unit,
|
||||
onMessageChange: (String) -> Unit,
|
||||
textStyle: MutableState<TextStyle>
|
||||
) {
|
||||
@@ -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
|
||||
)
|
||||
|
||||
+12
-5
@@ -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(
|
||||
@@ -37,6 +37,7 @@ actual fun PlatformTextField(
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
onMessageChange: (String) -> Unit,
|
||||
onUpArrow: () -> Unit,
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
val cs = composeState.value
|
||||
@@ -73,17 +74,23 @@ 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()) {
|
||||
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 ->
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+44
@@ -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 ../../
|
||||
+45
-38
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+31
-11
@@ -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 <invitation> " <> " - accept invitation using incognito profile",
|
||||
indent <> highlight "/accept incognito <name> " <> " - 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 <invitation>", "/ac i <name>"],
|
||||
"To find the profile used for an incognito connection, use " <> highlight "/info <contact>" <> "."
|
||||
]
|
||||
|
||||
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 <path> " <> " - set files folder path to include app files in the exported archive",
|
||||
"",
|
||||
green "Database encryption:",
|
||||
indent <> highlight "/db encrypt <key> " <> " - encrypt chat database with key/passphrase",
|
||||
indent <> highlight "/db key <current> <new>" <> " - change the key of the encrypted app database",
|
||||
indent <> highlight "/db decrypt <key> " <> " - 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 <path> " <> " - set files folder path to include app files in the exported archive",
|
||||
"",
|
||||
green "Database encryption:",
|
||||
indent <> highlight "/db encrypt <key> " <> " - encrypt chat database with key/passphrase",
|
||||
indent <> highlight "/db key <current> <new>" <> " - change the key of the encrypted app database",
|
||||
indent <> highlight "/db decrypt <key> " <> " - decrypt chat database"
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 <> " <name> 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 <> "...")
|
||||
|
||||
+152
-41
@@ -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 <> " <whispering> 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 </)
|
||||
alice `hasContactProfiles` ["alice"]
|
||||
-- /_accept api
|
||||
cath ##> ("/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 <name> 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_
|
||||
|
||||
Reference in New Issue
Block a user