diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 878536069..b4301dcb3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,7 +81,7 @@ jobs: - name: Setup Haskell uses: haskell-actions/setup@v2 with: - ghc-version: "9.6.2" + ghc-version: "9.6.3" cabal-version: "3.10.1.0" - name: Cache dependencies diff --git a/Dockerfile b/Dockerfile index 0c0788c81..834f2374a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,11 @@ RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/ chmod +x /usr/bin/ghcup # Install ghc -RUN ghcup install ghc 9.6.2 +RUN ghcup install ghc 9.6.3 # Install cabal RUN ghcup install cabal 3.10.1.0 # Set both as default -RUN ghcup set ghc 9.6.2 && \ +RUN ghcup set ghc 9.6.3 && \ ghcup set cabal 3.10.1.0 COPY . /project diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 39f5df636..9e6073c10 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -120,6 +120,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { BGManager.shared.receiveMessages(complete) } + + static func keepScreenOn(_ on: Bool) { + UIApplication.shared.isIdleTimerDisabled = on + } } class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { diff --git a/apps/ios/Shared/Model/AudioRecPlay.swift b/apps/ios/Shared/Model/AudioRecPlay.swift index 973d79ab3..78773f5ea 100644 --- a/apps/ios/Shared/Model/AudioRecPlay.swift +++ b/apps/ios/Shared/Model/AudioRecPlay.swift @@ -46,6 +46,7 @@ class AudioRecorder { audioRecorder?.record(forDuration: MAX_VOICE_MESSAGE_LENGTH) await MainActor.run { + AppDelegate.keepScreenOn(true) recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in guard let time = self.audioRecorder?.currentTime else { return } self.onTimer?(time) @@ -57,6 +58,9 @@ class AudioRecorder { } return nil } catch let error { + await MainActor.run { + AppDelegate.keepScreenOn(false) + } logger.error("AudioRecorder startAudioRecording error \(error.localizedDescription)") return .error(error.localizedDescription) } @@ -71,6 +75,7 @@ class AudioRecorder { timer.invalidate() } recordingTimer = nil + AppDelegate.keepScreenOn(false) } private func checkPermission() async -> Bool { @@ -121,6 +126,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in if self.audioPlayer?.isPlaying ?? false { + AppDelegate.keepScreenOn(true) guard let time = self.audioPlayer?.currentTime else { return } self.onTimer?(time) } @@ -129,6 +135,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { func pause() { audioPlayer?.pause() + AppDelegate.keepScreenOn(false) } func play() { @@ -149,6 +156,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { func stop() { if let player = audioPlayer { player.stop() + AppDelegate.keepScreenOn(false) } audioPlayer = nil if let timer = playbackTimer { diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index 393a370ee..ad9d90c38 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -39,6 +39,7 @@ struct ActiveCallView: View { } .onAppear { logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)") + AppDelegate.keepScreenOn(true) createWebRTCClient() dismissAllSheets() } @@ -48,6 +49,7 @@ struct ActiveCallView: View { } .onDisappear { logger.debug("ActiveCallView: disappear") + AppDelegate.keepScreenOn(false) client?.endCall() } .onChange(of: m.callCommand) { _ in sendCommandToClient()} diff --git a/apps/ios/Shared/Views/Helpers/VideoPlayerView.swift b/apps/ios/Shared/Views/Helpers/VideoPlayerView.swift index 5b982f5f0..416fa0c37 100644 --- a/apps/ios/Shared/Views/Helpers/VideoPlayerView.swift +++ b/apps/ios/Shared/Views/Helpers/VideoPlayerView.swift @@ -6,6 +6,7 @@ import Foundation import SwiftUI import AVKit +import Combine struct VideoPlayerView: UIViewRepresentable { @@ -37,6 +38,9 @@ struct VideoPlayerView: UIViewRepresentable { player.seek(to: CMTime.zero) player.play() } + context.coordinator.publisher = player.publisher(for: \.timeControlStatus).sink { status in + AppDelegate.keepScreenOn(status == .playing) + } return controller.view } @@ -50,11 +54,13 @@ struct VideoPlayerView: UIViewRepresentable { class Coordinator: NSObject { var controller: AVPlayerViewController? var timeObserver: Any? = nil + var publisher: AnyCancellable? = nil deinit { if let timeObserver = timeObserver { NotificationCenter.default.removeObserver(timeObserver) } + publisher?.cancel() } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt index 8df99d15f..b89719b2e 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt @@ -48,6 +48,7 @@ actual class RecorderNative: RecorderInterface { recStartedAt = System.currentTimeMillis() progressJob = CoroutineScope(Dispatchers.Default).launch { while(isActive) { + keepScreenOn(true) onProgressUpdate(progress(), false) delay(50) } @@ -84,6 +85,7 @@ actual class RecorderNative: RecorderInterface { progressJob = null filePath = null recorder = null + keepScreenOn(false) return (realDuration(path) ?: 0).also { recStartedAt = null } } @@ -170,6 +172,7 @@ actual object AudioPlayer: AudioPlayerInterface { progressJob = CoroutineScope(Dispatchers.Default).launch { onProgressUpdate(player.currentPosition, TrackState.PLAYING) while(isActive && player.isPlaying) { + keepScreenOn(true) // Even when current position is equal to duration, the player has isPlaying == true for some time, // so help to make the playback stopped in UI immediately if (player.currentPosition == player.duration) { @@ -187,6 +190,7 @@ actual object AudioPlayer: AudioPlayerInterface { if (isActive) { onProgressUpdate(player.duration, TrackState.PAUSED) } + keepScreenOn(false) onProgressUpdate(null, TrackState.PAUSED) } return player.duration @@ -196,6 +200,7 @@ actual object AudioPlayer: AudioPlayerInterface { progressJob?.cancel() progressJob = null player.pause() + keepScreenOn(false) return player.currentPosition } @@ -203,6 +208,7 @@ actual object AudioPlayer: AudioPlayerInterface { if (currentlyPlaying.value == null) return player.stop() stopListener() + keepScreenOn(false) } override fun stop(item: ChatItem) = stop(item.file?.fileName) @@ -263,6 +269,7 @@ actual object AudioPlayer: AudioPlayerInterface { override fun pause(audioPlaying: MutableState, pro: MutableState) { pro.value = pause() audioPlaying.value = false + keepScreenOn(false) } override fun seekTo(ms: Int, pro: MutableState, filePath: String?) { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt index 574b756bb..1d62efbb4 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt @@ -1,13 +1,10 @@ package chat.simplex.common.platform -import android.media.MediaMetadataRetriever import android.media.session.PlaybackState import android.net.Uri import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import chat.simplex.common.helpers.toUri import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import com.google.android.exoplayer2.* @@ -134,6 +131,7 @@ actual class VideoPlayer actual constructor( player.addListener(object: Player.Listener{ override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) + keepScreenOn(isPlaying) // Produce non-ideal transition from stopped to playing state while showing preview image in ChatView // videoPlaying.value = isPlaying } @@ -192,6 +190,7 @@ actual class VideoPlayer actual constructor( override fun release(remove: Boolean) { player.release() + keepScreenOn(false) if (remove) { VideoPlayerHolder.players.remove(uri to gallery) } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index 790345e97..5c7b430ab 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -196,12 +196,14 @@ actual fun ActiveCallView() { chatModel.activeCallViewIsVisible.value = true // After the first call, End command gets added to the list which prevents making another calls chatModel.callCommand.removeAll { it is WCallCommand.End } + keepScreenOn(true) onDispose { activity.volumeControlStream = prevVolumeControlStream // Unlock orientation activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED chatModel.activeCallViewIsVisible.value = false chatModel.callCommand.clear() + keepScreenOn(false) } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index 8b98b0542..f2c2f393a 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -11,6 +11,7 @@ import android.text.Spanned import android.text.SpannedString import android.text.style.* import android.util.Base64 +import android.view.WindowManager import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.* @@ -43,6 +44,17 @@ fun Resources.getText(id: StringResource, vararg args: Any): CharSequence { return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) } +fun keepScreenOn(on: Boolean) { + val window = mainActivity.get()?.window ?: return + Handler(Looper.getMainLooper()).post { + if (on) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } +} + actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString { return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index dae79e6fc..5e64de2c5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -137,7 +137,7 @@ fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? { val destFileName = generateNewFileName("IMG", ext) val destFile = File(getAppFilePath(destFileName)) if (encrypted) { - val args = writeCryptoFile(destFile.absolutePath, uri.inputStream()?.readAllBytes() ?: return null) + val args = writeCryptoFile(destFile.absolutePath, uri.inputStream()?.readBytes() ?: return null) CryptoFile(destFileName, args) } else { Files.copy(uri.inputStream(), destFile.toPath()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/CreateLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/CreateLinkView.kt index 08afbd4c6..94538655b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/CreateLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/CreateLinkView.kt @@ -11,8 +11,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatModel import chat.simplex.common.model.PendingContactConnection -import chat.simplex.common.views.helpers.ModalManager -import chat.simplex.common.views.helpers.withApi +import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.UserAddressView import chat.simplex.res.MR @@ -24,7 +23,7 @@ enum class CreateLinkTab { fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { val selection = remember { mutableStateOf(initialSelection) } val connReqInvitation = rememberSaveable { m.connReqInv } - val contactConnection: MutableState = rememberSaveable { mutableStateOf(null) } + val contactConnection: MutableState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) } val creatingConnReq = rememberSaveable { mutableStateOf(false) } LaunchedEffect(selection.value) { if ( diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index c04587656..0df5ee815 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -170,9 +170,8 @@ actual fun isAnimImage(uri: URI, drawable: Any?): Boolean { return path.endsWith(".gif") || path.endsWith(".webp") } -@Suppress("NewApi") actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap = - Image.makeFromEncoded(inputStream.readAllBytes()).toComposeImageBitmap() + Image.makeFromEncoded(inputStream.readBytes()).toComposeImageBitmap() // https://stackoverflow.com/a/68926993 fun BufferedImage.rotate(angle: Double): BufferedImage { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt index 42def0c75..cce8a3ce8 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -189,12 +189,11 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit): NanoWSD { val server = object: NanoWSD(SERVER_HOST, SERVER_PORT) { override fun openWebSocket(session: IHTTPSession): WebSocket = MyWebSocket(onResponse, session) - @Suppress("NewApi") fun resourcesToResponse(path: String): Response { val uri = Class.forName("chat.simplex.common.AppKt").getResource("/assets/www$path") ?: return resourceNotFound val response = newFixedLengthResponse( Status.OK, getMimeTypeForFile(uri.file), - uri.openStream().readAllBytes() + uri.openStream().readBytes() ) response.setKeepAlive(true) response.setUseGzip(true) diff --git a/docs/CLI.md b/docs/CLI.md index 7966627c4..f781f8574 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -102,7 +102,7 @@ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . #### In any OS -1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 9.6.2 and cabal 3.10.1.0: +1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 9.6.3 and cabal 3.10.1.0: ```shell curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 0aa09c516..aaf452af0 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -30,29 +30,29 @@ You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order t **In simplex-chat repo** -- `stable` - stable release of the apps, can be used for updates to the previous stable release (GHC 9.6.2). +- `stable` - stable release of the apps, can be used for updates to the previous stable release (GHC 9.6.3). -- `stable-android` - used to build stable Android core library with Nix (GHC 8.10.7). +- `stable-android` - used to build stable Android core library with Nix (GHC 8.10.7) - only for Android armv7a. -- `stable-ios` - used to build stable iOS core library with Nix (GHC 8.10.7) – this branch should be the same as `stable-android` except Nix configuration files. +- `stable-ios` - used to build stable iOS core library with Nix (GHC 8.10.7) – this branch should be the same as `stable-android` except Nix configuration files. Deprecated. -- `master` - branch for beta version releases (GHC 9.6.2). +- `master` - branch for beta version releases (GHC 9.6.3). -- `master-ghc8107` - branch for beta version releases (GHC 8.10.7). +- `master-ghc8107` - branch for beta version releases (GHC 8.10.7). Deprecated. -- `master-android` - used to build beta Android core library with Nix (GHC 8.10.7), same as `master-ghc8107` +- `master-android` - used to build beta Android core library with Nix (GHC 8.10.7) - only for Android armv7a. -- `master-ios` - used to build beta iOS core library with Nix (GHC 8.10.7). +- `master-ios` - used to build beta iOS core library with Nix (GHC 8.10.7). Deprecated. -- `windows-ghc8107` - branch for windows core library build (GHC 8.10.7). +- `windows-ghc8107` - branch for windows core library build (GHC 8.10.7). Deprecated? `master-ios` and `windows-ghc8107` branches should be the same as `master-ghc8107` except Nix configuration files. **In simplexmq repo** -- `master` - uses GHC 9.6.2 its commit should be used in `master` branch of simplex-chat repo. +- `master` - uses GHC 9.6.3 its commit should be used in `master` branch of simplex-chat repo. -- `master-ghc8107` - its commit should be used in `master-android` (and `master-ios`) branch of simplex-chat repo. +- `master-ghc8107` - its commit should be used in `master-android` (and `master-ios`) branch of simplex-chat repo. Deprecated. ## Development & release process @@ -61,53 +61,44 @@ You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order t 2. If simplexmq repo was changed, to build mobile core libraries you need to merge its `master` branch into `master-ghc8107` branch. 3. To build core libraries for Android, iOS and windows: -- merge `master` branch to `master-ghc8107` branch. -- update `simplexmq` commit in `master-ghc8107` branch to the commit in `master-ghc8107` branch (probably, when resolving merge conflicts). +- merge `master` branch to `master-android` branch. - update code to be compatible with GHC 8.10.7 (see below). - push to GitHub. -4. To build Android core library, merge `master-ghc8107` branch to `master-android` branch, and push to GitHub. +4. All libraries should be built from `master` branch, Android armv7a - from `master-android` branch. -5. To build iOS core library, merge `master-ghc8107` branch to `master-ios` branch, and push to GitHub. +5. To build Desktop and CLI apps, make tag in `master` branch, APK files should be attached to the release. -6. To build windows core library, merge `master-ghc8107` branch to `windows-ghc8107` branch, and push to GitHub. - -7. To build Desktop and CLI apps, make tag in `master` branch, APK files should be attached to the release. - -8. After the public release to App Store and Play Store, merge: +6. After the public release to App Store and Play Store, merge: - `master` to `stable` -- `master` to `master-ghc8107` (and compile/update code) -- `master-ghc8107` to `master-android` -- `master-ghc8107` to `master-ios` -- `master-ghc8107` to `windows-ghc8107` +- `master` to `master-android` (and compile/update code) - `master-android` to `stable-android` -- `master-ios` to `stable-ios` -9. Independently, `master` branch of simplexmq repo should be merged to `stable` branch on stable releases. +7. Independently, `master` branch of simplexmq repo should be merged to `stable` branch on stable releases. -## Differences between GHC 8.10.7 and GHC 9.6.2 +## Differences between GHC 8.10.7 and GHC 9.6.3 1. The main difference is related to `DuplicateRecordFields` extension. -It is no longer possible in GHC 9.6.2 to specify type when using selectors, instead OverloadedRecordDot extension and syntax are used that need to be removed in GHC 8.10.7: +It is no longer possible in GHC 9.6.3 to specify type when using selectors, instead OverloadedRecordDot extension and syntax are used that need to be removed in GHC 8.10.7: ```haskell {-# LANGUAGE DuplicateRecordFields #-} --- use this in GHC 9.6.2 when needed +-- use this in GHC 9.6.3 when needed {-# LANGUAGE OverloadedRecordDot #-} --- GHC 9.6.2 syntax +-- GHC 9.6.3 syntax let x = record.field --- GHC 8.10.7 syntax removed in GHC 9.6.2 +-- GHC 8.10.7 syntax removed in GHC 9.6.3 let x = field (record :: Record) ``` It is still possible to specify type when using record update syntax, use this pragma to suppress compiler warning: ```haskell --- use this in GHC 9.6.2 when needed +-- use this in GHC 9.6.3 when needed {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} let r' = (record :: Record) {field = value} @@ -116,7 +107,7 @@ let r' = (record :: Record) {field = value} 2. Most monad functions now have to be imported from `Control.Monad`, and not from specific monad modules (e.g. `Control.Monad.Except`). ```haskell --- use this in GHC 9.6.2 when needed +-- use this in GHC 9.6.3 when needed import Control.Monad ``` diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index e0ee7e669..fa1f892a0 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -8,7 +8,7 @@ function readlink() { OS=linux ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`} -GHC_VERSION=9.6.2 +GHC_VERSION=9.6.3 if [ "$ARCH" == "aarch64" ]; then COMPOSE_ARCH=arm64 diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index c33f59253..1a4deced4 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -5,7 +5,7 @@ set -e OS=mac ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}" COMPOSE_ARCH=$ARCH -GHC_VERSION=9.6.2 +GHC_VERSION=9.6.3 if [ "$ARCH" == "arm64" ]; then ARCH=aarch64 diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 5b2a59e29..32711d385 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -121,7 +121,8 @@ library Simplex.Chat.Migrations.M20231010_member_settings Simplex.Chat.Migrations.M20231019_indexes Simplex.Chat.Migrations.M20231030_xgrplinkmem_received - Simplex.Chat.Migrations.M20231031_remote_controller + Simplex.Chat.Migrations.M20231107_indexes + Simplex.Chat.Migrations.M20231114_remote_controller Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 2c9de6ac7..b9663bae1 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -436,6 +436,7 @@ processChatCommand = \case withAgent (\a -> createUser a smp xftp) ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts + when (auId == 1) $ withStore (\db -> createContact db user simplexContactProfile) `catchChatError` \_ -> pure () storeServers user smpServers storeServers user xftpServers atomically . writeTVar u $ Just user @@ -966,7 +967,7 @@ processChatCommand = \case deleteFilesAndConns user filesInfo when (contactReady ct && contactActive ct && notify) $ void (sendDirectContactMessage ct XDirectDel) `catchChatError` const (pure ()) - contactConnIds <- map aConnId <$> withStore (\db -> getContactConnections db userId ct) + contactConnIds <- map aConnId <$> withStore' (\db -> getContactConnections db userId ct) deleteAgentConnectionsAsync user contactConnIds -- functions below are called in separate transactions to prevent crashes on android -- (possibly, race condition on integrity check?) @@ -1009,7 +1010,7 @@ processChatCommand = \case withStore' (\db -> checkContactHasGroups db user ct) >>= \case Just _ -> pure [] Nothing -> do - conns <- withStore $ \db -> getContactConnections db userId ct + conns <- withStore' $ \db -> getContactConnections db userId ct withStore' (\db -> setContactDeleted db user ct) `catchChatError` (toView . CRChatError (Just user)) pure $ map aConnId conns @@ -1425,13 +1426,25 @@ processChatCommand = \case Connect incognito aCReqUri@(Just cReqUri) -> withUser $ \user@User {userId} -> do plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk) unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan) - processChatCommand $ APIConnect userId incognito aCReqUri + case plan of + CPContactAddress (CAPContactViaAddress Contact {contactId}) -> + processChatCommand $ APIConnectContactViaAddress userId incognito contactId + _ -> processChatCommand $ APIConnect userId incognito aCReqUri Connect _ Nothing -> throwChatError CEInvalidConnReq + APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do + ct@Contact {activeConn, profile = LocalProfile {contactLink}} <- withStore $ \db -> getContact db user contactId + when (isJust activeConn) $ throwChatError (CECommandError "contact already has connection") + case contactLink of + Just cReq -> connectContactViaAddress user incognito ct cReq + Nothing -> throwChatError (CECommandError "no address in contact profile") ConnectSimplex incognito -> withUser $ \user@User {userId} -> do let cReqUri = ACR SCMContact adminContactReq plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk) unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan) - processChatCommand $ APIConnect userId incognito (Just cReqUri) + case plan of + CPContactAddress (CAPContactViaAddress Contact {contactId}) -> + processChatCommand $ APIConnectContactViaAddress userId incognito contactId + _ -> processChatCommand $ APIConnect userId incognito (Just cReqUri) DeleteContact cName -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) True ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect APIListContacts userId -> withUserId userId $ \user -> @@ -2071,15 +2084,27 @@ processChatCommand = \case connect' (Just gLinkId) cReqHash xContactId where connect' groupLinkId cReqHash xContactId = do - -- [incognito] generate profile to send - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - let profileToSend = userProfileToSend user incognitoProfile Nothing - dm <- directMessage (XContact profileToSend $ Just xContactId) - subMode <- chatReadVar subscriptionMode - connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode + (connId, incognitoProfile, subMode) <- requestContact user incognito cReq xContactId conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode toView $ CRNewContactConnection user conn pure $ CRSentInvitation user incognitoProfile + connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> m ChatResponse + connectContactViaAddress user incognito ct cReq = + withChatLock "connectViaContact" $ do + newXContactId <- XContactId <$> drgRandomBytes 16 + (connId, incognitoProfile, subMode) <- requestContact user incognito cReq newXContactId + let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq + ct' <- withStore $ \db -> createAddressContactConnection db user ct connId cReqHash newXContactId incognitoProfile subMode + pure $ CRSentInvitationToContact user ct' incognitoProfile + requestContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> XContactId -> m (ConnId, Maybe Profile, SubscriptionMode) + requestContact user incognito cReq xContactId = do + -- [incognito] generate profile to send + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + let profileToSend = userProfileToSend user incognitoProfile Nothing + dm <- directMessage (XContact profileToSend $ Just xContactId) + subMode <- chatReadVar subscriptionMode + connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode + pure (connId, incognitoProfile, subMode) contactMember :: Contact -> [GroupMember] -> Maybe GroupMember contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> @@ -2332,9 +2357,12 @@ processChatCommand = \case Nothing -> withStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case Just _ -> pure $ CPContactAddress CAPOwnLink - Nothing -> do + Nothing -> withStore' (\db -> getContactConnEntityByConnReqHash db user cReqHashes) >>= \case - Nothing -> pure $ CPContactAddress CAPOk + Nothing -> + withStore' (\db -> getContactWithoutConnViaAddress db user cReqSchemas) >>= \case + Nothing -> pure $ CPContactAddress CAPOk + Just ct -> pure $ CPContactAddress (CAPContactViaAddress ct) Just (RcvDirectMsgConnection _conn Nothing) -> pure $ CPContactAddress CAPConnectingConfirmReconnect Just (RcvDirectMsgConnection _ (Just ct)) | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) @@ -4504,7 +4532,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do then do checkIntegrityCreateItem (CDDirectRcv c) msgMeta ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted - contactConns <- withStore $ \db -> getContactConnections db userId ct' + contactConns <- withStore' $ \db -> getContactConnections db userId ct' deleteAgentConnectionsAsync user $ map aConnId contactConns forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted} @@ -4513,7 +4541,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct'') ci) toView $ CRContactDeletedByContact user ct'' else do - contactConns <- withStore $ \db -> getContactConnections db userId c + contactConns <- withStore' $ \db -> getContactConnections db userId c deleteAgentConnectionsAsync user $ map aConnId contactConns withStore' $ \db -> deleteContact db user c @@ -5917,6 +5945,7 @@ chatCommandP = "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> optional (" encrypt=" *> onOffP)), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), + "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), "/simplex" *> (ConnectSimplex <$> incognitoP), "/_address " *> (APICreateMyAddress <$> A.decimal), ("/address" <|> "/ad") $> CreateMyAddress, @@ -6098,6 +6127,15 @@ adminContactReq :: ConnReqContact adminContactReq = either error id $ strDecode "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" +simplexContactProfile :: Profile +simplexContactProfile = Profile { + displayName = "SimpleX Chat team", + fullName = "", + image = Just (ImageData "data:image/jpg;base64,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"), + contactLink = Just adminContactReq, + preferences = Nothing +} + timeItToView :: ChatMonad' m => String -> m a -> m a timeItToView s action = do t1 <- liftIO getCurrentTime diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 06a421f60..6ca541a71 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -344,6 +344,7 @@ data ChatCommand | APIConnectPlan UserId AConnectionRequestUri | APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri) | Connect IncognitoEnabled (Maybe AConnectionRequestUri) + | APIConnectContactViaAddress UserId IncognitoEnabled ContactId | ConnectSimplex IncognitoEnabled -- UserId (not used in UI) | DeleteContact ContactName | ClearContact ContactName @@ -541,6 +542,7 @@ data ChatResponse | CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan} | CRSentConfirmation {user :: User} | CRSentInvitation {user :: User, customUserProfile :: Maybe Profile} + | CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile} | CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact} | CRGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember} | CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact, updatedContact :: Contact} @@ -724,6 +726,7 @@ data ContactAddressPlan | CAPConnectingConfirmReconnect | CAPConnectingProhibit {contact :: Contact} | CAPKnown {contact :: Contact} + | CAPContactViaAddress {contact :: Contact} deriving (Show) data GroupLinkPlan @@ -744,6 +747,7 @@ connectionPlanProceed = \case CAPOk -> True CAPOwnLink -> True CAPConnectingConfirmReconnect -> True + CAPContactViaAddress _ -> True _ -> False CPGroupLink glp -> case glp of GLPOk -> True diff --git a/src/Simplex/Chat/Migrations/M20231107_indexes.hs b/src/Simplex/Chat/Migrations/M20231107_indexes.hs new file mode 100644 index 000000000..a4c9c5295 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20231107_indexes.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20231107_indexes where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20231107_indexes :: Query +m20231107_indexes = + [sql| +CREATE INDEX idx_contact_profiles_contact_link ON contact_profiles(user_id, contact_link); +|] + +down_m20231107_indexes :: Query +down_m20231107_indexes = + [sql| +DROP INDEX idx_contact_profiles_contact_link; +|] diff --git a/src/Simplex/Chat/Migrations/M20231031_remote_controller.hs b/src/Simplex/Chat/Migrations/M20231114_remote_controller.hs similarity index 84% rename from src/Simplex/Chat/Migrations/M20231031_remote_controller.hs rename to src/Simplex/Chat/Migrations/M20231114_remote_controller.hs index dfae744a3..5b7ea1c7b 100644 --- a/src/Simplex/Chat/Migrations/M20231031_remote_controller.hs +++ b/src/Simplex/Chat/Migrations/M20231114_remote_controller.hs @@ -1,12 +1,12 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231031_remote_controller where +module Simplex.Chat.Migrations.M20231114_remote_controller where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) -m20231031_remote_controller :: Query -m20231031_remote_controller = +m20231114_remote_controller :: Query +m20231114_remote_controller = [sql| CREATE TABLE remote_hosts ( -- hosts known to a controlling app remote_host_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -25,8 +25,8 @@ CREATE TABLE remote_controllers ( -- controllers known to a hosting app ); |] -down_m20231031_remote_controller :: Query -down_m20231031_remote_controller = +down_m20231114_remote_controller :: Query +down_m20231114_remote_controller = [sql| DROP TABLE remote_hosts; DROP TABLE remote_controllers; diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 018af68c9..ebe3fc111 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -764,3 +764,7 @@ CREATE INDEX idx_connections_via_contact_uri_hash ON connections( user_id, via_contact_uri_hash ); +CREATE INDEX idx_contact_profiles_contact_link ON contact_profiles( + user_id, + contact_link +); diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index ba420b980..bfc29fcd2 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -23,6 +23,7 @@ module Simplex.Chat.Store.Direct createDirectConnection, createIncognitoProfile, createConnReqConnection, + createAddressContactConnection, getProfileById, getConnReqContactXContactId, getContactByConnReqHash, @@ -119,6 +120,12 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) +createAddressContactConnection :: DB.Connection -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> ExceptT StoreError IO Contact +createAddressContactConnection db user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode = do + PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode + liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId) + getContact db user contactId + createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> IO PendingContactConnection createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode = do createdAt <- getCurrentTime @@ -195,12 +202,13 @@ createIncognitoProfile db User {userId} p = do createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p@Profile {preferences} = do - createdAt <- liftIO getCurrentTime - (localDisplayName, contactId, profileId) <- createContact_ db userId connId p localAlias Nothing createdAt (Just createdAt) + currentTs <- liftIO getCurrentTime + (localDisplayName, contactId, profileId) <- createContact_ db userId p localAlias Nothing currentTs (Just currentTs) + liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId) let profile = toLocalProfile profileId p localAlias userPreferences = emptyChatPrefs mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn - pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed = False, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False} + pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed = False, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False} deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO () deleteContactConnectionsAndFiles db userId Contact {contactId} = do @@ -678,17 +686,20 @@ getContact_ db user@User {userId} contactId deleted = LEFT JOIN connections c ON c.contact_id = ct.contact_id WHERE ct.user_id = ? AND ct.contact_id = ? AND ct.deleted = ? - AND c.connection_id = ( - SELECT cc_connection_id FROM ( - SELECT - cc.connection_id AS cc_connection_id, - cc.created_at AS cc_created_at, - (CASE WHEN cc.conn_status = ? OR cc.conn_status = ? THEN 1 ELSE 0 END) AS cc_conn_status_ord - FROM connections cc - WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id - ORDER BY cc_conn_status_ord DESC, cc_created_at DESC - LIMIT 1 + AND ( + c.connection_id = ( + SELECT cc_connection_id FROM ( + SELECT + cc.connection_id AS cc_connection_id, + cc.created_at AS cc_created_at, + (CASE WHEN cc.conn_status = ? OR cc.conn_status = ? THEN 1 ELSE 0 END) AS cc_conn_status_ord + FROM connections cc + WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id + ORDER BY cc_conn_status_ord DESC, cc_created_at DESC + LIMIT 1 + ) ) + OR c.connection_id IS NULL ) |] (userId, contactId, deleted, ConnReady, ConnSndReady) @@ -712,7 +723,7 @@ getPendingContactConnections db User {userId} = do |] [":user_id" := userId, ":conn_type" := ConnContact] -getContactConnections :: DB.Connection -> UserId -> Contact -> ExceptT StoreError IO [Connection] +getContactConnections :: DB.Connection -> UserId -> Contact -> IO [Connection] getContactConnections db userId Contact {contactId} = connections =<< liftIO getConnections_ where @@ -728,7 +739,7 @@ getContactConnections db userId Contact {contactId} = WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ? |] (userId, userId, contactId) - connections [] = throwError $ SEContactNotFound contactId + connections [] = pure [] connections rows = pure $ map toConnection rows getConnectionById :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Connection diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 09c59eee6..40294dc14 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -1054,7 +1054,8 @@ createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupM Just (directCmdId, directAgentConnId) -> do Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode liftIO $ setCommandConnId db user directCmdId directConnId - (localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile "" (Just groupId) currentTs Nothing + (localDisplayName, contactId, memProfileId) <- createContact_ db userId memberProfile "" (Just groupId) currentTs Nothing + liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, directConnId) pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, localDisplayName, memContactId = Just contactId, memProfileId} Nothing -> do (localDisplayName, memProfileId) <- createNewMemberProfile_ db user memberProfile currentTs diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index e18afce64..18e6bc9be 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -87,7 +87,8 @@ import Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash import Simplex.Chat.Migrations.M20231010_member_settings import Simplex.Chat.Migrations.M20231019_indexes import Simplex.Chat.Migrations.M20231030_xgrplinkmem_received -import Simplex.Chat.Migrations.M20231031_remote_controller +import Simplex.Chat.Migrations.M20231107_indexes +import Simplex.Chat.Migrations.M20231114_remote_controller import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -175,7 +176,8 @@ schemaMigrations = ("20231010_member_settings", m20231010_member_settings, Just down_m20231010_member_settings), ("20231019_indexes", m20231019_indexes, Just down_m20231019_indexes), ("20231030_xgrplinkmem_received", m20231030_xgrplinkmem_received, Just down_m20231030_xgrplinkmem_received), - ("20231031_remote_controller", m20231031_remote_controller, Just down_m20231031_remote_controller) + ("20231107_indexes", m20231107_indexes, Just down_m20231107_indexes), + ("20231114_remote_controller", m20231114_remote_controller, Just down_m20231114_remote_controller) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 99689b29d..611faf90c 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -5,6 +5,7 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} @@ -44,6 +45,7 @@ module Simplex.Chat.Store.Profiles getUserAddress, getUserContactLinkById, getUserContactLinkByConnReq, + getContactWithoutConnViaAddress, updateUserAddressAutoAccept, getProtocolServers, overwriteProtocolServers, @@ -87,7 +89,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON) import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode) import Simplex.Messaging.Transport.Client (TransportHost) -import Simplex.Messaging.Util (safeDecodeUtf8) +import Simplex.Messaging.Util (safeDecodeUtf8, eitherToMaybe) createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime @@ -453,6 +455,21 @@ getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) = |] (userId, cReqSchema1, cReqSchema2) +getContactWithoutConnViaAddress :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) +getContactWithoutConnViaAddress db user@User {userId} (cReqSchema1, cReqSchema2) = do + ctId_ <- maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT ct.contact_id + FROM contacts ct + JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id + LEFT JOIN connections c ON c.contact_id = ct.contact_id + WHERE cp.user_id = ? AND cp.contact_link IN (?,?) AND c.connection_id IS NULL + |] + (userId, cReqSchema1, cReqSchema2) + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db user) ctId_ + updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink updateUserAddressAutoAccept db user@User {userId} autoAccept = do link <- getUserAddress db user diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 87d0cc416..e72d68b8d 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -217,8 +217,13 @@ setCommandConnId db User {userId} cmdId connId = do |] (connId, updatedAt, userId, cmdId) -createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> Maybe UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) -createContact_ db userId connId Profile {displayName, fullName, image, contactLink, preferences} localAlias viaGroup currentTs chatTs = +createContact :: DB.Connection -> User -> Profile -> ExceptT StoreError IO () +createContact db User {userId} profile = do + currentTs <- liftIO getCurrentTime + void $ createContact_ db userId profile "" Nothing currentTs Nothing + +createContact_ :: DB.Connection -> UserId -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> Maybe UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) +createContact_ db userId Profile {displayName, fullName, image, contactLink, preferences} localAlias viaGroup currentTs chatTs = ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do DB.execute db @@ -230,7 +235,6 @@ createContact_ db userId connId Profile {displayName, fullName, image, contactLi "INSERT INTO contacts (contact_profile_id, local_display_name, user_id, via_group, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?)" (profileId, ldn, userId, viaGroup, currentTs, currentTs, chatTs) contactId <- insertedRowId db - DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId) pure $ Right (ldn, contactId, profileId) deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO () diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index ea6905ca0..572403e59 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -161,6 +161,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan CRSentConfirmation u -> ttyUser u ["confirmation sent!"] CRSentInvitation u customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView + CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"] CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"] CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo @@ -1343,6 +1344,7 @@ viewConnectionPlan = \case [ ctAddr ("known contact " <> ttyContact' ct), "use " <> ttyToContact' ct <> highlight' "" <> " to send messages" ] + CAPContactViaAddress ct -> [ctAddr ("known contact without connection " <> ttyContact' ct)] where ctAddr = ("contact address: " <>) CPGroupLink glp -> case glp of diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index d806290d6..0a45a74ad 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -7,10 +7,16 @@ import ChatClient import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) +import Control.Monad.Except +import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Char8 as B import qualified Data.Text as T import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..), Profile (..)) import System.Directory (copyFile, createDirectoryIfMissing) import Test.Hspec +import Simplex.Chat.Store.Shared (createContact) +import Control.Monad +import Simplex.Messaging.Encoding.String (StrEncoding(..)) chatProfileTests :: SpecWith FilePath chatProfileTests = do @@ -33,6 +39,7 @@ chatProfileTests = do it "own contact address" testPlanAddressOwn it "connecting via contact address" testPlanAddressConnecting it "re-connect with deleted contact" testPlanAddressContactDeletedReconnected + it "contact via address" testPlanAddressContactViaAddress describe "incognito" $ do it "connect incognito via invitation link" testConnectIncognitoInvitationLink it "connect incognito via contact address" testConnectIncognitoContactAddress @@ -755,6 +762,66 @@ testPlanAddressContactDeletedReconnected = bob <## "contact address: known contact alice_1" bob <## "use @alice_1 to send messages" +testPlanAddressContactViaAddress :: HasCallStack => FilePath -> IO () +testPlanAddressContactViaAddress = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/ad" + cLink <- getContactLink alice True + + alice ##> "/pa on" -- not necessary, without it bob would receive profile update removing contact link + alice <## "new contact address set" + + case A.parseOnly strP (B.pack cLink) of + Left _ -> error "error parsing contact link" + Right cReq -> do + let profile = aliceProfile {contactLink = Just cReq} + void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile + bob @@@ [("@alice", "")] + + bob ##> "/delete @alice" + bob <## "alice: contact is deleted" + + void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile + bob @@@ [("@alice", "")] + + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: known contact without connection alice" + + let cLinkSchema2 = linkAnotherSchema cLink + bob ##> ("/_connect plan 1 " <> cLinkSchema2) + bob <## "contact address: known contact without connection alice" + + -- terminal api + bob ##> ("/c " <> cLink) + connecting alice bob + + bob ##> "/_delete @2 notify=off" + bob <## "alice: contact is deleted" + alice ##> "/_delete @2 notify=off" + alice <## "bob: contact is deleted" + + void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile + bob @@@ [("@alice", "")] + + -- GUI api + bob ##> "/_connect contact 1 2" + connecting alice bob + where + connecting alice bob = do + bob <## "connection request sent!" + alice <## "bob (Bob) wants to connect to you!" + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request..." + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + + alice <##> bob + bob @@@ [("@alice", "hey")] + testConnectIncognitoInvitationLink :: HasCallStack => FilePath -> IO () testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index d9a20d2d2..a2aff4bf5 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -438,6 +438,17 @@ getContactProfiles cc = do profiles <- withTransaction (chatStore $ chatController cc) $ \db -> getUserContactProfiles db user pure $ map (\Profile {displayName} -> displayName) profiles +withCCUser :: TestCC -> (User -> IO a) -> IO a +withCCUser cc action = do + user_ <- readTVarIO (currentUser $ chatController cc) + case user_ of + Nothing -> error "no user" + Just user -> action user + +withCCTransaction :: TestCC -> (DB.Connection -> IO a) -> IO a +withCCTransaction cc action = + withTransaction (chatStore $ chatController cc) $ \db -> action db + getProfilePictureByName :: TestCC -> String -> IO (Maybe String) getProfilePictureByName cc displayName = withTransaction (chatStore $ chatController cc) $ \db ->