Merge branch 'master' into remote-desktop
This commit is contained in:
commit
3839267f88
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -81,7 +81,7 @@ jobs:
|
|||||||
- name: Setup Haskell
|
- name: Setup Haskell
|
||||||
uses: haskell-actions/setup@v2
|
uses: haskell-actions/setup@v2
|
||||||
with:
|
with:
|
||||||
ghc-version: "9.6.2"
|
ghc-version: "9.6.3"
|
||||||
cabal-version: "3.10.1.0"
|
cabal-version: "3.10.1.0"
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
|
@ -8,11 +8,11 @@ RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/
|
|||||||
chmod +x /usr/bin/ghcup
|
chmod +x /usr/bin/ghcup
|
||||||
|
|
||||||
# Install ghc
|
# Install ghc
|
||||||
RUN ghcup install ghc 9.6.2
|
RUN ghcup install ghc 9.6.3
|
||||||
# Install cabal
|
# Install cabal
|
||||||
RUN ghcup install cabal 3.10.1.0
|
RUN ghcup install cabal 3.10.1.0
|
||||||
# Set both as default
|
# 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
|
ghcup set cabal 3.10.1.0
|
||||||
|
|
||||||
COPY . /project
|
COPY . /project
|
||||||
|
@ -120,6 +120,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||||||
|
|
||||||
BGManager.shared.receiveMessages(complete)
|
BGManager.shared.receiveMessages(complete)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func keepScreenOn(_ on: Bool) {
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = on
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
||||||
|
@ -46,6 +46,7 @@ class AudioRecorder {
|
|||||||
audioRecorder?.record(forDuration: MAX_VOICE_MESSAGE_LENGTH)
|
audioRecorder?.record(forDuration: MAX_VOICE_MESSAGE_LENGTH)
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
AppDelegate.keepScreenOn(true)
|
||||||
recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
|
recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
|
||||||
guard let time = self.audioRecorder?.currentTime else { return }
|
guard let time = self.audioRecorder?.currentTime else { return }
|
||||||
self.onTimer?(time)
|
self.onTimer?(time)
|
||||||
@ -57,6 +58,9 @@ class AudioRecorder {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
} catch let error {
|
} catch let error {
|
||||||
|
await MainActor.run {
|
||||||
|
AppDelegate.keepScreenOn(false)
|
||||||
|
}
|
||||||
logger.error("AudioRecorder startAudioRecording error \(error.localizedDescription)")
|
logger.error("AudioRecorder startAudioRecording error \(error.localizedDescription)")
|
||||||
return .error(error.localizedDescription)
|
return .error(error.localizedDescription)
|
||||||
}
|
}
|
||||||
@ -71,6 +75,7 @@ class AudioRecorder {
|
|||||||
timer.invalidate()
|
timer.invalidate()
|
||||||
}
|
}
|
||||||
recordingTimer = nil
|
recordingTimer = nil
|
||||||
|
AppDelegate.keepScreenOn(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkPermission() async -> Bool {
|
private func checkPermission() async -> Bool {
|
||||||
@ -121,6 +126,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
|||||||
|
|
||||||
playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
|
playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
|
||||||
if self.audioPlayer?.isPlaying ?? false {
|
if self.audioPlayer?.isPlaying ?? false {
|
||||||
|
AppDelegate.keepScreenOn(true)
|
||||||
guard let time = self.audioPlayer?.currentTime else { return }
|
guard let time = self.audioPlayer?.currentTime else { return }
|
||||||
self.onTimer?(time)
|
self.onTimer?(time)
|
||||||
}
|
}
|
||||||
@ -129,6 +135,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
|||||||
|
|
||||||
func pause() {
|
func pause() {
|
||||||
audioPlayer?.pause()
|
audioPlayer?.pause()
|
||||||
|
AppDelegate.keepScreenOn(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func play() {
|
func play() {
|
||||||
@ -149,6 +156,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
|||||||
func stop() {
|
func stop() {
|
||||||
if let player = audioPlayer {
|
if let player = audioPlayer {
|
||||||
player.stop()
|
player.stop()
|
||||||
|
AppDelegate.keepScreenOn(false)
|
||||||
}
|
}
|
||||||
audioPlayer = nil
|
audioPlayer = nil
|
||||||
if let timer = playbackTimer {
|
if let timer = playbackTimer {
|
||||||
|
@ -39,6 +39,7 @@ struct ActiveCallView: View {
|
|||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)")
|
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)")
|
||||||
|
AppDelegate.keepScreenOn(true)
|
||||||
createWebRTCClient()
|
createWebRTCClient()
|
||||||
dismissAllSheets()
|
dismissAllSheets()
|
||||||
}
|
}
|
||||||
@ -48,6 +49,7 @@ struct ActiveCallView: View {
|
|||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
logger.debug("ActiveCallView: disappear")
|
logger.debug("ActiveCallView: disappear")
|
||||||
|
AppDelegate.keepScreenOn(false)
|
||||||
client?.endCall()
|
client?.endCall()
|
||||||
}
|
}
|
||||||
.onChange(of: m.callCommand) { _ in sendCommandToClient()}
|
.onChange(of: m.callCommand) { _ in sendCommandToClient()}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AVKit
|
import AVKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
struct VideoPlayerView: UIViewRepresentable {
|
struct VideoPlayerView: UIViewRepresentable {
|
||||||
|
|
||||||
@ -37,6 +38,9 @@ struct VideoPlayerView: UIViewRepresentable {
|
|||||||
player.seek(to: CMTime.zero)
|
player.seek(to: CMTime.zero)
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
|
context.coordinator.publisher = player.publisher(for: \.timeControlStatus).sink { status in
|
||||||
|
AppDelegate.keepScreenOn(status == .playing)
|
||||||
|
}
|
||||||
return controller.view
|
return controller.view
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,11 +54,13 @@ struct VideoPlayerView: UIViewRepresentable {
|
|||||||
class Coordinator: NSObject {
|
class Coordinator: NSObject {
|
||||||
var controller: AVPlayerViewController?
|
var controller: AVPlayerViewController?
|
||||||
var timeObserver: Any? = nil
|
var timeObserver: Any? = nil
|
||||||
|
var publisher: AnyCancellable? = nil
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
if let timeObserver = timeObserver {
|
if let timeObserver = timeObserver {
|
||||||
NotificationCenter.default.removeObserver(timeObserver)
|
NotificationCenter.default.removeObserver(timeObserver)
|
||||||
}
|
}
|
||||||
|
publisher?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ actual class RecorderNative: RecorderInterface {
|
|||||||
recStartedAt = System.currentTimeMillis()
|
recStartedAt = System.currentTimeMillis()
|
||||||
progressJob = CoroutineScope(Dispatchers.Default).launch {
|
progressJob = CoroutineScope(Dispatchers.Default).launch {
|
||||||
while(isActive) {
|
while(isActive) {
|
||||||
|
keepScreenOn(true)
|
||||||
onProgressUpdate(progress(), false)
|
onProgressUpdate(progress(), false)
|
||||||
delay(50)
|
delay(50)
|
||||||
}
|
}
|
||||||
@ -84,6 +85,7 @@ actual class RecorderNative: RecorderInterface {
|
|||||||
progressJob = null
|
progressJob = null
|
||||||
filePath = null
|
filePath = null
|
||||||
recorder = null
|
recorder = null
|
||||||
|
keepScreenOn(false)
|
||||||
return (realDuration(path) ?: 0).also { recStartedAt = null }
|
return (realDuration(path) ?: 0).also { recStartedAt = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +172,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
|||||||
progressJob = CoroutineScope(Dispatchers.Default).launch {
|
progressJob = CoroutineScope(Dispatchers.Default).launch {
|
||||||
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
|
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
|
||||||
while(isActive && player.isPlaying) {
|
while(isActive && player.isPlaying) {
|
||||||
|
keepScreenOn(true)
|
||||||
// Even when current position is equal to duration, the player has isPlaying == true for some time,
|
// 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
|
// so help to make the playback stopped in UI immediately
|
||||||
if (player.currentPosition == player.duration) {
|
if (player.currentPosition == player.duration) {
|
||||||
@ -187,6 +190,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
|||||||
if (isActive) {
|
if (isActive) {
|
||||||
onProgressUpdate(player.duration, TrackState.PAUSED)
|
onProgressUpdate(player.duration, TrackState.PAUSED)
|
||||||
}
|
}
|
||||||
|
keepScreenOn(false)
|
||||||
onProgressUpdate(null, TrackState.PAUSED)
|
onProgressUpdate(null, TrackState.PAUSED)
|
||||||
}
|
}
|
||||||
return player.duration
|
return player.duration
|
||||||
@ -196,6 +200,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
|||||||
progressJob?.cancel()
|
progressJob?.cancel()
|
||||||
progressJob = null
|
progressJob = null
|
||||||
player.pause()
|
player.pause()
|
||||||
|
keepScreenOn(false)
|
||||||
return player.currentPosition
|
return player.currentPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,6 +208,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
|||||||
if (currentlyPlaying.value == null) return
|
if (currentlyPlaying.value == null) return
|
||||||
player.stop()
|
player.stop()
|
||||||
stopListener()
|
stopListener()
|
||||||
|
keepScreenOn(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop(item: ChatItem) = stop(item.file?.fileName)
|
override fun stop(item: ChatItem) = stop(item.file?.fileName)
|
||||||
@ -263,6 +269,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
|||||||
override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) {
|
override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) {
|
||||||
pro.value = pause()
|
pro.value = pause()
|
||||||
audioPlaying.value = false
|
audioPlaying.value = false
|
||||||
|
keepScreenOn(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) {
|
override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) {
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
package chat.simplex.common.platform
|
package chat.simplex.common.platform
|
||||||
|
|
||||||
import android.media.MediaMetadataRetriever
|
|
||||||
import android.media.session.PlaybackState
|
import android.media.session.PlaybackState
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
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.common.views.helpers.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
@ -134,6 +131,7 @@ actual class VideoPlayer actual constructor(
|
|||||||
player.addListener(object: Player.Listener{
|
player.addListener(object: Player.Listener{
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
super.onIsPlayingChanged(isPlaying)
|
super.onIsPlayingChanged(isPlaying)
|
||||||
|
keepScreenOn(isPlaying)
|
||||||
// Produce non-ideal transition from stopped to playing state while showing preview image in ChatView
|
// Produce non-ideal transition from stopped to playing state while showing preview image in ChatView
|
||||||
// videoPlaying.value = isPlaying
|
// videoPlaying.value = isPlaying
|
||||||
}
|
}
|
||||||
@ -192,6 +190,7 @@ actual class VideoPlayer actual constructor(
|
|||||||
|
|
||||||
override fun release(remove: Boolean) {
|
override fun release(remove: Boolean) {
|
||||||
player.release()
|
player.release()
|
||||||
|
keepScreenOn(false)
|
||||||
if (remove) {
|
if (remove) {
|
||||||
VideoPlayerHolder.players.remove(uri to gallery)
|
VideoPlayerHolder.players.remove(uri to gallery)
|
||||||
}
|
}
|
||||||
|
@ -196,12 +196,14 @@ actual fun ActiveCallView() {
|
|||||||
chatModel.activeCallViewIsVisible.value = true
|
chatModel.activeCallViewIsVisible.value = true
|
||||||
// After the first call, End command gets added to the list which prevents making another calls
|
// After the first call, End command gets added to the list which prevents making another calls
|
||||||
chatModel.callCommand.removeAll { it is WCallCommand.End }
|
chatModel.callCommand.removeAll { it is WCallCommand.End }
|
||||||
|
keepScreenOn(true)
|
||||||
onDispose {
|
onDispose {
|
||||||
activity.volumeControlStream = prevVolumeControlStream
|
activity.volumeControlStream = prevVolumeControlStream
|
||||||
// Unlock orientation
|
// Unlock orientation
|
||||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
chatModel.activeCallViewIsVisible.value = false
|
chatModel.activeCallViewIsVisible.value = false
|
||||||
chatModel.callCommand.clear()
|
chatModel.callCommand.clear()
|
||||||
|
keepScreenOn(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import android.text.Spanned
|
|||||||
import android.text.SpannedString
|
import android.text.SpannedString
|
||||||
import android.text.style.*
|
import android.text.style.*
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.*
|
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)
|
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 {
|
actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString {
|
||||||
return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density)
|
return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density)
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? {
|
|||||||
val destFileName = generateNewFileName("IMG", ext)
|
val destFileName = generateNewFileName("IMG", ext)
|
||||||
val destFile = File(getAppFilePath(destFileName))
|
val destFile = File(getAppFilePath(destFileName))
|
||||||
if (encrypted) {
|
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)
|
CryptoFile(destFileName, args)
|
||||||
} else {
|
} else {
|
||||||
Files.copy(uri.inputStream(), destFile.toPath())
|
Files.copy(uri.inputStream(), destFile.toPath())
|
||||||
|
@ -11,8 +11,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.common.model.ChatModel
|
import chat.simplex.common.model.ChatModel
|
||||||
import chat.simplex.common.model.PendingContactConnection
|
import chat.simplex.common.model.PendingContactConnection
|
||||||
import chat.simplex.common.views.helpers.ModalManager
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.helpers.withApi
|
|
||||||
import chat.simplex.common.views.usersettings.UserAddressView
|
import chat.simplex.common.views.usersettings.UserAddressView
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
|
||||||
@ -24,7 +23,7 @@ enum class CreateLinkTab {
|
|||||||
fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
||||||
val selection = remember { mutableStateOf(initialSelection) }
|
val selection = remember { mutableStateOf(initialSelection) }
|
||||||
val connReqInvitation = rememberSaveable { m.connReqInv }
|
val connReqInvitation = rememberSaveable { m.connReqInv }
|
||||||
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable { mutableStateOf(null) }
|
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) }
|
||||||
val creatingConnReq = rememberSaveable { mutableStateOf(false) }
|
val creatingConnReq = rememberSaveable { mutableStateOf(false) }
|
||||||
LaunchedEffect(selection.value) {
|
LaunchedEffect(selection.value) {
|
||||||
if (
|
if (
|
||||||
|
@ -170,9 +170,8 @@ actual fun isAnimImage(uri: URI, drawable: Any?): Boolean {
|
|||||||
return path.endsWith(".gif") || path.endsWith(".webp")
|
return path.endsWith(".gif") || path.endsWith(".webp")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NewApi")
|
|
||||||
actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap =
|
actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap =
|
||||||
Image.makeFromEncoded(inputStream.readAllBytes()).toComposeImageBitmap()
|
Image.makeFromEncoded(inputStream.readBytes()).toComposeImageBitmap()
|
||||||
|
|
||||||
// https://stackoverflow.com/a/68926993
|
// https://stackoverflow.com/a/68926993
|
||||||
fun BufferedImage.rotate(angle: Double): BufferedImage {
|
fun BufferedImage.rotate(angle: Double): BufferedImage {
|
||||||
|
@ -189,12 +189,11 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit): NanoWSD {
|
|||||||
val server = object: NanoWSD(SERVER_HOST, SERVER_PORT) {
|
val server = object: NanoWSD(SERVER_HOST, SERVER_PORT) {
|
||||||
override fun openWebSocket(session: IHTTPSession): WebSocket = MyWebSocket(onResponse, session)
|
override fun openWebSocket(session: IHTTPSession): WebSocket = MyWebSocket(onResponse, session)
|
||||||
|
|
||||||
@Suppress("NewApi")
|
|
||||||
fun resourcesToResponse(path: String): Response {
|
fun resourcesToResponse(path: String): Response {
|
||||||
val uri = Class.forName("chat.simplex.common.AppKt").getResource("/assets/www$path") ?: return resourceNotFound
|
val uri = Class.forName("chat.simplex.common.AppKt").getResource("/assets/www$path") ?: return resourceNotFound
|
||||||
val response = newFixedLengthResponse(
|
val response = newFixedLengthResponse(
|
||||||
Status.OK, getMimeTypeForFile(uri.file),
|
Status.OK, getMimeTypeForFile(uri.file),
|
||||||
uri.openStream().readAllBytes()
|
uri.openStream().readBytes()
|
||||||
)
|
)
|
||||||
response.setKeepAlive(true)
|
response.setKeepAlive(true)
|
||||||
response.setUseGzip(true)
|
response.setUseGzip(true)
|
||||||
|
@ -102,7 +102,7 @@ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
|
|||||||
|
|
||||||
#### In any OS
|
#### 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
|
```shell
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
||||||
|
@ -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**
|
**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.
|
`master-ios` and `windows-ghc8107` branches should be the same as `master-ghc8107` except Nix configuration files.
|
||||||
|
|
||||||
**In simplexmq repo**
|
**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
|
## 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.
|
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:
|
3. To build core libraries for Android, iOS and windows:
|
||||||
- merge `master` branch to `master-ghc8107` branch.
|
- merge `master` branch to `master-android` branch.
|
||||||
- update `simplexmq` commit in `master-ghc8107` branch to the commit in `master-ghc8107` branch (probably, when resolving merge conflicts).
|
|
||||||
- update code to be compatible with GHC 8.10.7 (see below).
|
- update code to be compatible with GHC 8.10.7 (see below).
|
||||||
- push to GitHub.
|
- 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.
|
6. After the public release to App Store and Play Store, merge:
|
||||||
|
|
||||||
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:
|
|
||||||
- `master` to `stable`
|
- `master` to `stable`
|
||||||
- `master` to `master-ghc8107` (and compile/update code)
|
- `master` to `master-android` (and compile/update code)
|
||||||
- `master-ghc8107` to `master-android`
|
|
||||||
- `master-ghc8107` to `master-ios`
|
|
||||||
- `master-ghc8107` to `windows-ghc8107`
|
|
||||||
- `master-android` to `stable-android`
|
- `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.
|
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
|
```haskell
|
||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
-- use this in GHC 9.6.2 when needed
|
-- use this in GHC 9.6.3 when needed
|
||||||
{-# LANGUAGE OverloadedRecordDot #-}
|
{-# LANGUAGE OverloadedRecordDot #-}
|
||||||
|
|
||||||
-- GHC 9.6.2 syntax
|
-- GHC 9.6.3 syntax
|
||||||
let x = record.field
|
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)
|
let x = field (record :: Record)
|
||||||
```
|
```
|
||||||
|
|
||||||
It is still possible to specify type when using record update syntax, use this pragma to suppress compiler warning:
|
It is still possible to specify type when using record update syntax, use this pragma to suppress compiler warning:
|
||||||
|
|
||||||
```haskell
|
```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 #-}
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||||
|
|
||||||
let r' = (record :: Record) {field = value}
|
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`).
|
2. Most monad functions now have to be imported from `Control.Monad`, and not from specific monad modules (e.g. `Control.Monad.Except`).
|
||||||
|
|
||||||
```haskell
|
```haskell
|
||||||
-- use this in GHC 9.6.2 when needed
|
-- use this in GHC 9.6.3 when needed
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ function readlink() {
|
|||||||
|
|
||||||
OS=linux
|
OS=linux
|
||||||
ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`}
|
ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`}
|
||||||
GHC_VERSION=9.6.2
|
GHC_VERSION=9.6.3
|
||||||
|
|
||||||
if [ "$ARCH" == "aarch64" ]; then
|
if [ "$ARCH" == "aarch64" ]; then
|
||||||
COMPOSE_ARCH=arm64
|
COMPOSE_ARCH=arm64
|
||||||
|
@ -5,7 +5,7 @@ set -e
|
|||||||
OS=mac
|
OS=mac
|
||||||
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
|
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
|
||||||
COMPOSE_ARCH=$ARCH
|
COMPOSE_ARCH=$ARCH
|
||||||
GHC_VERSION=9.6.2
|
GHC_VERSION=9.6.3
|
||||||
|
|
||||||
if [ "$ARCH" == "arm64" ]; then
|
if [ "$ARCH" == "arm64" ]; then
|
||||||
ARCH=aarch64
|
ARCH=aarch64
|
||||||
|
@ -121,7 +121,8 @@ library
|
|||||||
Simplex.Chat.Migrations.M20231010_member_settings
|
Simplex.Chat.Migrations.M20231010_member_settings
|
||||||
Simplex.Chat.Migrations.M20231019_indexes
|
Simplex.Chat.Migrations.M20231019_indexes
|
||||||
Simplex.Chat.Migrations.M20231030_xgrplinkmem_received
|
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
|
||||||
Simplex.Chat.Mobile.File
|
Simplex.Chat.Mobile.File
|
||||||
Simplex.Chat.Mobile.Shared
|
Simplex.Chat.Mobile.Shared
|
||||||
|
File diff suppressed because one or more lines are too long
@ -344,6 +344,7 @@ data ChatCommand
|
|||||||
| APIConnectPlan UserId AConnectionRequestUri
|
| APIConnectPlan UserId AConnectionRequestUri
|
||||||
| APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri)
|
| APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri)
|
||||||
| Connect IncognitoEnabled (Maybe AConnectionRequestUri)
|
| Connect IncognitoEnabled (Maybe AConnectionRequestUri)
|
||||||
|
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId
|
||||||
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
|
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
|
||||||
| DeleteContact ContactName
|
| DeleteContact ContactName
|
||||||
| ClearContact ContactName
|
| ClearContact ContactName
|
||||||
@ -541,6 +542,7 @@ data ChatResponse
|
|||||||
| CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan}
|
| CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan}
|
||||||
| CRSentConfirmation {user :: User}
|
| CRSentConfirmation {user :: User}
|
||||||
| CRSentInvitation {user :: User, customUserProfile :: Maybe Profile}
|
| CRSentInvitation {user :: User, customUserProfile :: Maybe Profile}
|
||||||
|
| CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile}
|
||||||
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
||||||
| CRGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember}
|
| CRGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember}
|
||||||
| CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact, updatedContact :: Contact}
|
| CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact, updatedContact :: Contact}
|
||||||
@ -724,6 +726,7 @@ data ContactAddressPlan
|
|||||||
| CAPConnectingConfirmReconnect
|
| CAPConnectingConfirmReconnect
|
||||||
| CAPConnectingProhibit {contact :: Contact}
|
| CAPConnectingProhibit {contact :: Contact}
|
||||||
| CAPKnown {contact :: Contact}
|
| CAPKnown {contact :: Contact}
|
||||||
|
| CAPContactViaAddress {contact :: Contact}
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data GroupLinkPlan
|
data GroupLinkPlan
|
||||||
@ -744,6 +747,7 @@ connectionPlanProceed = \case
|
|||||||
CAPOk -> True
|
CAPOk -> True
|
||||||
CAPOwnLink -> True
|
CAPOwnLink -> True
|
||||||
CAPConnectingConfirmReconnect -> True
|
CAPConnectingConfirmReconnect -> True
|
||||||
|
CAPContactViaAddress _ -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
CPGroupLink glp -> case glp of
|
CPGroupLink glp -> case glp of
|
||||||
GLPOk -> True
|
GLPOk -> True
|
||||||
|
18
src/Simplex/Chat/Migrations/M20231107_indexes.hs
Normal file
18
src/Simplex/Chat/Migrations/M20231107_indexes.hs
Normal file
@ -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;
|
||||||
|
|]
|
@ -1,12 +1,12 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# 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 (Query)
|
||||||
import Database.SQLite.Simple.QQ (sql)
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
|
|
||||||
m20231031_remote_controller :: Query
|
m20231114_remote_controller :: Query
|
||||||
m20231031_remote_controller =
|
m20231114_remote_controller =
|
||||||
[sql|
|
[sql|
|
||||||
CREATE TABLE remote_hosts ( -- hosts known to a controlling app
|
CREATE TABLE remote_hosts ( -- hosts known to a controlling app
|
||||||
remote_host_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
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_m20231114_remote_controller :: Query
|
||||||
down_m20231031_remote_controller =
|
down_m20231114_remote_controller =
|
||||||
[sql|
|
[sql|
|
||||||
DROP TABLE remote_hosts;
|
DROP TABLE remote_hosts;
|
||||||
DROP TABLE remote_controllers;
|
DROP TABLE remote_controllers;
|
@ -764,3 +764,7 @@ CREATE INDEX idx_connections_via_contact_uri_hash ON connections(
|
|||||||
user_id,
|
user_id,
|
||||||
via_contact_uri_hash
|
via_contact_uri_hash
|
||||||
);
|
);
|
||||||
|
CREATE INDEX idx_contact_profiles_contact_link ON contact_profiles(
|
||||||
|
user_id,
|
||||||
|
contact_link
|
||||||
|
);
|
||||||
|
@ -23,6 +23,7 @@ module Simplex.Chat.Store.Direct
|
|||||||
createDirectConnection,
|
createDirectConnection,
|
||||||
createIncognitoProfile,
|
createIncognitoProfile,
|
||||||
createConnReqConnection,
|
createConnReqConnection,
|
||||||
|
createAddressContactConnection,
|
||||||
getProfileById,
|
getProfileById,
|
||||||
getConnReqContactXContactId,
|
getConnReqContactXContactId,
|
||||||
getContactByConnReqHash,
|
getContactByConnReqHash,
|
||||||
@ -119,6 +120,12 @@ deletePendingContactConnection db userId connId =
|
|||||||
|]
|
|]
|
||||||
(userId, connId, ConnContact)
|
(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.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> IO PendingContactConnection
|
||||||
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode = do
|
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode = do
|
||||||
createdAt <- getCurrentTime
|
createdAt <- getCurrentTime
|
||||||
@ -195,12 +202,13 @@ createIncognitoProfile db User {userId} p = do
|
|||||||
|
|
||||||
createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact
|
createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact
|
||||||
createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p@Profile {preferences} = do
|
createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p@Profile {preferences} = do
|
||||||
createdAt <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
(localDisplayName, contactId, profileId) <- createContact_ db userId connId p localAlias Nothing createdAt (Just createdAt)
|
(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
|
let profile = toLocalProfile profileId p localAlias
|
||||||
userPreferences = emptyChatPrefs
|
userPreferences = emptyChatPrefs
|
||||||
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
|
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.Connection -> UserId -> Contact -> IO ()
|
||||||
deleteContactConnectionsAndFiles db userId Contact {contactId} = do
|
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
|
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||||
WHERE ct.user_id = ? AND ct.contact_id = ?
|
WHERE ct.user_id = ? AND ct.contact_id = ?
|
||||||
AND ct.deleted = ?
|
AND ct.deleted = ?
|
||||||
AND c.connection_id = (
|
AND (
|
||||||
SELECT cc_connection_id FROM (
|
c.connection_id = (
|
||||||
SELECT
|
SELECT cc_connection_id FROM (
|
||||||
cc.connection_id AS cc_connection_id,
|
SELECT
|
||||||
cc.created_at AS cc_created_at,
|
cc.connection_id AS cc_connection_id,
|
||||||
(CASE WHEN cc.conn_status = ? OR cc.conn_status = ? THEN 1 ELSE 0 END) AS cc_conn_status_ord
|
cc.created_at AS cc_created_at,
|
||||||
FROM connections cc
|
(CASE WHEN cc.conn_status = ? OR cc.conn_status = ? THEN 1 ELSE 0 END) AS cc_conn_status_ord
|
||||||
WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id
|
FROM connections cc
|
||||||
ORDER BY cc_conn_status_ord DESC, cc_created_at DESC
|
WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id
|
||||||
LIMIT 1
|
ORDER BY cc_conn_status_ord DESC, cc_created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
OR c.connection_id IS NULL
|
||||||
)
|
)
|
||||||
|]
|
|]
|
||||||
(userId, contactId, deleted, ConnReady, ConnSndReady)
|
(userId, contactId, deleted, ConnReady, ConnSndReady)
|
||||||
@ -712,7 +723,7 @@ getPendingContactConnections db User {userId} = do
|
|||||||
|]
|
|]
|
||||||
[":user_id" := userId, ":conn_type" := ConnContact]
|
[":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} =
|
getContactConnections db userId Contact {contactId} =
|
||||||
connections =<< liftIO getConnections_
|
connections =<< liftIO getConnections_
|
||||||
where
|
where
|
||||||
@ -728,7 +739,7 @@ getContactConnections db userId Contact {contactId} =
|
|||||||
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
|
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
|
||||||
|]
|
|]
|
||||||
(userId, userId, contactId)
|
(userId, userId, contactId)
|
||||||
connections [] = throwError $ SEContactNotFound contactId
|
connections [] = pure []
|
||||||
connections rows = pure $ map toConnection rows
|
connections rows = pure $ map toConnection rows
|
||||||
|
|
||||||
getConnectionById :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Connection
|
getConnectionById :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Connection
|
||||||
|
@ -1054,7 +1054,8 @@ createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupM
|
|||||||
Just (directCmdId, directAgentConnId) -> do
|
Just (directCmdId, directAgentConnId) -> do
|
||||||
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode
|
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode
|
||||||
liftIO $ setCommandConnId db user directCmdId directConnId
|
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}
|
pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, localDisplayName, memContactId = Just contactId, memProfileId}
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
(localDisplayName, memProfileId) <- createNewMemberProfile_ db user memberProfile currentTs
|
(localDisplayName, memProfileId) <- createNewMemberProfile_ db user memberProfile currentTs
|
||||||
|
@ -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.M20231010_member_settings
|
||||||
import Simplex.Chat.Migrations.M20231019_indexes
|
import Simplex.Chat.Migrations.M20231019_indexes
|
||||||
import Simplex.Chat.Migrations.M20231030_xgrplinkmem_received
|
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 (..))
|
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||||
|
|
||||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||||
@ -175,7 +176,8 @@ schemaMigrations =
|
|||||||
("20231010_member_settings", m20231010_member_settings, Just down_m20231010_member_settings),
|
("20231010_member_settings", m20231010_member_settings, Just down_m20231010_member_settings),
|
||||||
("20231019_indexes", m20231019_indexes, Just down_m20231019_indexes),
|
("20231019_indexes", m20231019_indexes, Just down_m20231019_indexes),
|
||||||
("20231030_xgrplinkmem_received", m20231030_xgrplinkmem_received, Just down_m20231030_xgrplinkmem_received),
|
("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
|
-- | The list of migrations in ascending order by date
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
{-# LANGUAGE TupleSections #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
{-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ module Simplex.Chat.Store.Profiles
|
|||||||
getUserAddress,
|
getUserAddress,
|
||||||
getUserContactLinkById,
|
getUserContactLinkById,
|
||||||
getUserContactLinkByConnReq,
|
getUserContactLinkByConnReq,
|
||||||
|
getContactWithoutConnViaAddress,
|
||||||
updateUserAddressAutoAccept,
|
updateUserAddressAutoAccept,
|
||||||
getProtocolServers,
|
getProtocolServers,
|
||||||
overwriteProtocolServers,
|
overwriteProtocolServers,
|
||||||
@ -87,7 +89,7 @@ import Simplex.Messaging.Encoding.String
|
|||||||
import Simplex.Messaging.Parsers (defaultJSON)
|
import Simplex.Messaging.Parsers (defaultJSON)
|
||||||
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode)
|
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode)
|
||||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
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.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User
|
||||||
createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime
|
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)
|
(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.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink
|
||||||
updateUserAddressAutoAccept db user@User {userId} autoAccept = do
|
updateUserAddressAutoAccept db user@User {userId} autoAccept = do
|
||||||
link <- getUserAddress db user
|
link <- getUserAddress db user
|
||||||
|
@ -217,8 +217,13 @@ setCommandConnId db User {userId} cmdId connId = do
|
|||||||
|]
|
|]
|
||||||
(connId, updatedAt, userId, cmdId)
|
(connId, updatedAt, userId, cmdId)
|
||||||
|
|
||||||
createContact_ :: DB.Connection -> UserId -> Int64 -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> Maybe UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId)
|
createContact :: DB.Connection -> User -> Profile -> ExceptT StoreError IO ()
|
||||||
createContact_ db userId connId Profile {displayName, fullName, image, contactLink, preferences} localAlias viaGroup currentTs chatTs =
|
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
|
ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
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 (?,?,?,?,?,?,?)"
|
"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)
|
(profileId, ldn, userId, viaGroup, currentTs, currentTs, chatTs)
|
||||||
contactId <- insertedRowId db
|
contactId <- insertedRowId db
|
||||||
DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId)
|
|
||||||
pure $ Right (ldn, contactId, profileId)
|
pure $ Right (ldn, contactId, profileId)
|
||||||
|
|
||||||
deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO ()
|
deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO ()
|
||||||
|
@ -161,6 +161,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||||||
CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan
|
CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan
|
||||||
CRSentConfirmation u -> ttyUser u ["confirmation sent!"]
|
CRSentConfirmation u -> ttyUser u ["confirmation sent!"]
|
||||||
CRSentInvitation u customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
|
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"]
|
CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"]
|
||||||
CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"]
|
CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"]
|
||||||
CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo
|
CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo
|
||||||
@ -1343,6 +1344,7 @@ viewConnectionPlan = \case
|
|||||||
[ ctAddr ("known contact " <> ttyContact' ct),
|
[ ctAddr ("known contact " <> ttyContact' ct),
|
||||||
"use " <> ttyToContact' ct <> highlight' "<message>" <> " to send messages"
|
"use " <> ttyToContact' ct <> highlight' "<message>" <> " to send messages"
|
||||||
]
|
]
|
||||||
|
CAPContactViaAddress ct -> [ctAddr ("known contact without connection " <> ttyContact' ct)]
|
||||||
where
|
where
|
||||||
ctAddr = ("contact address: " <>)
|
ctAddr = ("contact address: " <>)
|
||||||
CPGroupLink glp -> case glp of
|
CPGroupLink glp -> case glp of
|
||||||
|
@ -7,10 +7,16 @@ import ChatClient
|
|||||||
import ChatTests.Utils
|
import ChatTests.Utils
|
||||||
import Control.Concurrent (threadDelay)
|
import Control.Concurrent (threadDelay)
|
||||||
import Control.Concurrent.Async (concurrently_)
|
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 qualified Data.Text as T
|
||||||
import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..), Profile (..))
|
import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..), Profile (..))
|
||||||
import System.Directory (copyFile, createDirectoryIfMissing)
|
import System.Directory (copyFile, createDirectoryIfMissing)
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
import Simplex.Chat.Store.Shared (createContact)
|
||||||
|
import Control.Monad
|
||||||
|
import Simplex.Messaging.Encoding.String (StrEncoding(..))
|
||||||
|
|
||||||
chatProfileTests :: SpecWith FilePath
|
chatProfileTests :: SpecWith FilePath
|
||||||
chatProfileTests = do
|
chatProfileTests = do
|
||||||
@ -33,6 +39,7 @@ chatProfileTests = do
|
|||||||
it "own contact address" testPlanAddressOwn
|
it "own contact address" testPlanAddressOwn
|
||||||
it "connecting via contact address" testPlanAddressConnecting
|
it "connecting via contact address" testPlanAddressConnecting
|
||||||
it "re-connect with deleted contact" testPlanAddressContactDeletedReconnected
|
it "re-connect with deleted contact" testPlanAddressContactDeletedReconnected
|
||||||
|
it "contact via address" testPlanAddressContactViaAddress
|
||||||
describe "incognito" $ do
|
describe "incognito" $ do
|
||||||
it "connect incognito via invitation link" testConnectIncognitoInvitationLink
|
it "connect incognito via invitation link" testConnectIncognitoInvitationLink
|
||||||
it "connect incognito via contact address" testConnectIncognitoContactAddress
|
it "connect incognito via contact address" testConnectIncognitoContactAddress
|
||||||
@ -755,6 +762,66 @@ testPlanAddressContactDeletedReconnected =
|
|||||||
bob <## "contact address: known contact alice_1"
|
bob <## "contact address: known contact alice_1"
|
||||||
bob <## "use @alice_1 <message> to send messages"
|
bob <## "use @alice_1 <message> 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 :: HasCallStack => FilePath -> IO ()
|
||||||
testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfile $
|
testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfile $
|
||||||
\alice bob cath -> do
|
\alice bob cath -> do
|
||||||
|
@ -438,6 +438,17 @@ getContactProfiles cc = do
|
|||||||
profiles <- withTransaction (chatStore $ chatController cc) $ \db -> getUserContactProfiles db user
|
profiles <- withTransaction (chatStore $ chatController cc) $ \db -> getUserContactProfiles db user
|
||||||
pure $ map (\Profile {displayName} -> displayName) profiles
|
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 :: TestCC -> String -> IO (Maybe String)
|
||||||
getProfilePictureByName cc displayName =
|
getProfilePictureByName cc displayName =
|
||||||
withTransaction (chatStore $ chatController cc) $ \db ->
|
withTransaction (chatStore $ chatController cc) $ \db ->
|
||||||
|
Loading…
Reference in New Issue
Block a user