Merge branch 'master' into master-ghc8107
This commit is contained in:
commit
98a3fc214d
@ -97,7 +97,7 @@ kotlin {
|
|||||||
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6")
|
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6")
|
||||||
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
|
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
|
||||||
implementation("org.slf4j:slf4j-simple:2.0.7")
|
implementation("org.slf4j:slf4j-simple:2.0.7")
|
||||||
implementation("uk.co.caprica:vlcj:4.7.0")
|
implementation("uk.co.caprica:vlcj:4.7.3")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val desktopTest by getting
|
val desktopTest by getting
|
||||||
|
@ -73,6 +73,11 @@ else()
|
|||||||
target_link_libraries(app-lib rts simplex)
|
target_link_libraries(app-lib rts simplex)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(APPLE)
|
||||||
|
add_custom_command(TARGET app-lib POST_BUILD
|
||||||
|
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/patch-libapp-mac.sh
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
# Trying to copy resulting files into needed directory, but none of these work for some reason. This could allow to
|
# Trying to copy resulting files into needed directory, but none of these work for some reason. This could allow to
|
||||||
|
8
apps/multiplatform/common/src/commonMain/cpp/desktop/patch-libapp-mac.sh
Executable file
8
apps/multiplatform/common/src/commonMain/cpp/desktop/patch-libapp-mac.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
lib=libapp-lib.dylib
|
||||||
|
RPATHS=$(otool -l $lib | grep -E '/Users|/opt/|/usr/local' | cut -d' ' -f11)
|
||||||
|
for RPATH in $RPATHS; do
|
||||||
|
install_name_tool -delete_rpath $RPATH $lib
|
||||||
|
done
|
@ -516,8 +516,8 @@ object ChatController {
|
|||||||
throw Exception("failed to delete the user ${r.responseType} ${r.details}")
|
throw Exception("failed to delete the user ${r.responseType} ${r.details}")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiStartChat(): Boolean {
|
suspend fun apiStartChat(openDBWithKey: String? = null): Boolean {
|
||||||
val r = sendCmd(CC.StartChat(expire = true))
|
val r = sendCmd(CC.StartChat(ChatCtrlCfg(subConns = true, enableExpireCIs = true, startXFTPWorkers = true, openDBWithKey = openDBWithKey)))
|
||||||
when (r) {
|
when (r) {
|
||||||
is CR.ChatStarted -> return true
|
is CR.ChatStarted -> return true
|
||||||
is CR.ChatRunning -> return false
|
is CR.ChatRunning -> return false
|
||||||
@ -525,8 +525,8 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiStopChat(): Boolean {
|
suspend fun apiStopChat(closeStore: Boolean): Boolean {
|
||||||
val r = sendCmd(CC.ApiStopChat())
|
val r = sendCmd(CC.ApiStopChat(closeStore))
|
||||||
when (r) {
|
when (r) {
|
||||||
is CR.ChatStopped -> return true
|
is CR.ChatStopped -> return true
|
||||||
else -> throw Error("failed stopping chat: ${r.responseType} ${r.details}")
|
else -> throw Error("failed stopping chat: ${r.responseType} ${r.details}")
|
||||||
@ -1829,8 +1829,8 @@ sealed class CC {
|
|||||||
class ApiMuteUser(val userId: Long): CC()
|
class ApiMuteUser(val userId: Long): CC()
|
||||||
class ApiUnmuteUser(val userId: Long): CC()
|
class ApiUnmuteUser(val userId: Long): CC()
|
||||||
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
|
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
|
||||||
class StartChat(val expire: Boolean): CC()
|
class StartChat(val cfg: ChatCtrlCfg): CC()
|
||||||
class ApiStopChat: CC()
|
class ApiStopChat(val closeStore: Boolean): CC()
|
||||||
class SetTempFolder(val tempFolder: String): CC()
|
class SetTempFolder(val tempFolder: String): CC()
|
||||||
class SetFilesFolder(val filesFolder: String): CC()
|
class SetFilesFolder(val filesFolder: String): CC()
|
||||||
class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC()
|
class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC()
|
||||||
@ -1933,8 +1933,9 @@ sealed class CC {
|
|||||||
is ApiMuteUser -> "/_mute user $userId"
|
is ApiMuteUser -> "/_mute user $userId"
|
||||||
is ApiUnmuteUser -> "/_unmute user $userId"
|
is ApiUnmuteUser -> "/_unmute user $userId"
|
||||||
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
|
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
|
||||||
is StartChat -> "/_start subscribe=on expire=${onOff(expire)} xftp=on"
|
// is StartChat -> "/_start ${json.encodeToString(cfg)}" // this can be used with the new core
|
||||||
is ApiStopChat -> "/_stop"
|
is StartChat -> "/_start subscribe=on expire=${onOff(cfg.enableExpireCIs)} xftp=on"
|
||||||
|
is ApiStopChat -> if (closeStore) "/_stop close" else "/_stop"
|
||||||
is SetTempFolder -> "/_temp_folder $tempFolder"
|
is SetTempFolder -> "/_temp_folder $tempFolder"
|
||||||
is SetFilesFolder -> "/_files_folder $filesFolder"
|
is SetFilesFolder -> "/_files_folder $filesFolder"
|
||||||
is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off"
|
is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off"
|
||||||
@ -2151,6 +2152,14 @@ sealed class CC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ChatCtrlCfg (
|
||||||
|
val subConns: Boolean,
|
||||||
|
val enableExpireCIs: Boolean,
|
||||||
|
val startXFTPWorkers: Boolean,
|
||||||
|
val openDBWithKey: String?
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NewUser(
|
data class NewUser(
|
||||||
val profile: Profile?,
|
val profile: Profile?,
|
||||||
|
@ -127,7 +127,7 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
|
|||||||
)
|
)
|
||||||
if (showPreview.value) {
|
if (showPreview.value) {
|
||||||
VideoPreviewImageView(preview, onClick, onLongClick)
|
VideoPreviewImageView(preview, onClick, onLongClick)
|
||||||
PlayButton(brokenVideo, onLongClick = onLongClick, if (appPlatform.isAndroid) play else onClick)
|
PlayButton(brokenVideo, onLongClick = onLongClick, play)
|
||||||
}
|
}
|
||||||
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
|
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
|
||||||
}
|
}
|
||||||
|
@ -158,12 +158,11 @@ private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap,
|
|||||||
player.stop()
|
player.stop()
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
player.enableSound(true)
|
|
||||||
snapshotFlow { isCurrentPage.value }
|
snapshotFlow { isCurrentPage.value }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect {
|
.collect {
|
||||||
// Do not autoplay on desktop because it needs workaround
|
if (it) play() else stop()
|
||||||
if (it && appPlatform.isAndroid) play() else if (!it) stop()
|
player.enableSound(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -419,7 +419,7 @@ private fun stopChat(m: ChatModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopChatAsync(m: ChatModel) {
|
suspend fun stopChatAsync(m: ChatModel) {
|
||||||
m.controller.apiStopChat()
|
m.controller.apiStopChat(false)
|
||||||
m.chatRunning.value = false
|
m.chatRunning.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
package org.jetbrains.compose.videoplayer
|
||||||
|
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asComposeImageBitmap
|
||||||
|
import org.jetbrains.skia.Bitmap
|
||||||
|
import org.jetbrains.skia.ColorAlphaType
|
||||||
|
import org.jetbrains.skia.ColorType
|
||||||
|
import org.jetbrains.skia.ImageInfo
|
||||||
|
import uk.co.caprica.vlcj.player.base.MediaPlayer
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.CallbackVideoSurface
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.VideoSurface
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.VideoSurfaceAdapters
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormat
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormatCallback
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.callback.RenderCallback
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.callback.format.RV32BufferFormat
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
// https://github.com/JetBrains/compose-multiplatform/pull/3336/files
|
||||||
|
internal class SkiaBitmapVideoSurface : VideoSurface(VideoSurfaceAdapters.getVideoSurfaceAdapter()) {
|
||||||
|
|
||||||
|
private val videoSurface = SkiaBitmapVideoSurface()
|
||||||
|
private lateinit var imageInfo: ImageInfo
|
||||||
|
private lateinit var frameBytes: ByteArray
|
||||||
|
private val skiaBitmap: Bitmap = Bitmap()
|
||||||
|
private val composeBitmap = mutableStateOf<ImageBitmap?>(null)
|
||||||
|
|
||||||
|
val bitmap: State<ImageBitmap?> = composeBitmap
|
||||||
|
|
||||||
|
override fun attach(mediaPlayer: MediaPlayer) {
|
||||||
|
videoSurface.attach(mediaPlayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SkiaBitmapBufferFormatCallback : BufferFormatCallback {
|
||||||
|
private var sourceWidth: Int = 0
|
||||||
|
private var sourceHeight: Int = 0
|
||||||
|
|
||||||
|
override fun getBufferFormat(sourceWidth: Int, sourceHeight: Int): BufferFormat {
|
||||||
|
this.sourceWidth = sourceWidth
|
||||||
|
this.sourceHeight = sourceHeight
|
||||||
|
return RV32BufferFormat(sourceWidth, sourceHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allocatedBuffers(buffers: Array<ByteBuffer>) {
|
||||||
|
frameBytes = buffers[0].run { ByteArray(remaining()).also(::get) }
|
||||||
|
imageInfo = ImageInfo(
|
||||||
|
sourceWidth,
|
||||||
|
sourceHeight,
|
||||||
|
ColorType.BGRA_8888,
|
||||||
|
ColorAlphaType.PREMUL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SkiaBitmapRenderCallback : RenderCallback {
|
||||||
|
override fun display(
|
||||||
|
mediaPlayer: MediaPlayer,
|
||||||
|
nativeBuffers: Array<ByteBuffer>,
|
||||||
|
bufferFormat: BufferFormat,
|
||||||
|
) {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
nativeBuffers[0].rewind()
|
||||||
|
nativeBuffers[0].get(frameBytes)
|
||||||
|
skiaBitmap.installPixels(imageInfo, frameBytes, bufferFormat.width * 4)
|
||||||
|
composeBitmap.value = skiaBitmap.asComposeImageBitmap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SkiaBitmapVideoSurface : CallbackVideoSurface(
|
||||||
|
SkiaBitmapBufferFormatCallback(),
|
||||||
|
SkiaBitmapRenderCallback(),
|
||||||
|
true,
|
||||||
|
videoSurfaceAdapter,
|
||||||
|
)
|
||||||
|
}
|
@ -6,6 +6,8 @@ import boofcv.struct.image.GrayU8
|
|||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import org.jetbrains.skia.Image
|
import org.jetbrains.skia.Image
|
||||||
import java.awt.RenderingHints
|
import java.awt.RenderingHints
|
||||||
|
import java.awt.geom.AffineTransform
|
||||||
|
import java.awt.image.AffineTransformOp
|
||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@ -171,3 +173,37 @@ actual fun isAnimImage(uri: URI, drawable: Any?): Boolean {
|
|||||||
@Suppress("NewApi")
|
@Suppress("NewApi")
|
||||||
actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap =
|
actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap =
|
||||||
Image.makeFromEncoded(inputStream.readAllBytes()).toComposeImageBitmap()
|
Image.makeFromEncoded(inputStream.readAllBytes()).toComposeImageBitmap()
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/68926993
|
||||||
|
fun BufferedImage.rotate(angle: Double): BufferedImage {
|
||||||
|
val sin = Math.abs(Math.sin(Math.toRadians(angle)))
|
||||||
|
val cos = Math.abs(Math.cos(Math.toRadians(angle)))
|
||||||
|
val w = width
|
||||||
|
val h = height
|
||||||
|
val neww = Math.floor(w * cos + h * sin).toInt()
|
||||||
|
val newh = Math.floor(h * cos + w * sin).toInt()
|
||||||
|
val rotated = BufferedImage(neww, newh, type)
|
||||||
|
val graphic = rotated.createGraphics()
|
||||||
|
graphic.translate((neww - w) / 2, (newh - h) / 2)
|
||||||
|
graphic.rotate(Math.toRadians(angle), (w / 2).toDouble(), (h / 2).toDouble())
|
||||||
|
graphic.drawRenderedImage(this, null)
|
||||||
|
graphic.dispose()
|
||||||
|
return rotated
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/9559043
|
||||||
|
fun BufferedImage.flip(vertically: Boolean, horizontally: Boolean): BufferedImage {
|
||||||
|
if (!vertically && !horizontally) return this
|
||||||
|
val tx: AffineTransform
|
||||||
|
if (vertically && horizontally) {
|
||||||
|
tx = AffineTransform.getScaleInstance(-1.0, -1.0)
|
||||||
|
tx.translate(-width.toDouble(), -height.toDouble())
|
||||||
|
} else if (vertically) {
|
||||||
|
tx = AffineTransform.getScaleInstance(1.0, -1.0)
|
||||||
|
tx.translate(0.0, -height.toDouble())
|
||||||
|
} else {
|
||||||
|
tx = AffineTransform.getScaleInstance(-1.0, 1.0)
|
||||||
|
tx.translate(-width.toDouble(), 0.0)
|
||||||
|
}
|
||||||
|
return AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(this, null)
|
||||||
|
}
|
||||||
|
@ -2,17 +2,20 @@ package chat.simplex.common.platform
|
|||||||
|
|
||||||
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.*
|
||||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import uk.co.caprica.vlcj.player.base.MediaPlayer
|
import uk.co.caprica.vlcj.media.VideoOrientation
|
||||||
|
import uk.co.caprica.vlcj.player.base.*
|
||||||
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
|
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
|
||||||
import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent
|
import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
actual class VideoPlayer actual constructor(
|
actual class VideoPlayer actual constructor(
|
||||||
@ -29,17 +32,14 @@ actual class VideoPlayer actual constructor(
|
|||||||
override val duration: MutableState<Long> = mutableStateOf(0L)
|
override val duration: MutableState<Long> = mutableStateOf(0L)
|
||||||
override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview)
|
override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview)
|
||||||
|
|
||||||
val mediaPlayerComponent = initializeMediaPlayerComponent()
|
val mediaPlayerComponent by lazy { runBlocking(playerThread.asCoroutineDispatcher()) { getOrCreatePlayer() } }
|
||||||
val player by lazy { mediaPlayerComponent.mediaPlayer() }
|
val player by lazy { mediaPlayerComponent.mediaPlayer() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
withBGApi {
|
setPreviewAndDuration()
|
||||||
setPreviewAndDuration()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val currentVolume: Int by lazy { player.audio().volume() }
|
private var isReleased: AtomicBoolean = AtomicBoolean(false)
|
||||||
private var isReleased: Boolean = false
|
|
||||||
|
|
||||||
private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null)
|
private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null)
|
||||||
private var progressJob: Job? = null
|
private var progressJob: Job? = null
|
||||||
@ -48,6 +48,7 @@ actual class VideoPlayer actual constructor(
|
|||||||
PLAYING, PAUSED, STOPPED
|
PLAYING, PAUSED, STOPPED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Should be called in [playerThread]. Otherwise, it creates deadlocks in [player.stop] and [player.release] calls */
|
||||||
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
|
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
|
||||||
val filepath = getAppFilePath(uri)
|
val filepath = getAppFilePath(uri)
|
||||||
if (filepath == null || !File(filepath).exists()) {
|
if (filepath == null || !File(filepath).exists()) {
|
||||||
@ -87,7 +88,7 @@ actual class VideoPlayer actual constructor(
|
|||||||
// Player can only be accessed in one specific thread
|
// Player can only be accessed in one specific thread
|
||||||
progressJob = CoroutineScope(Dispatchers.Main).launch {
|
progressJob = CoroutineScope(Dispatchers.Main).launch {
|
||||||
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
||||||
while (isActive && !isReleased && player.isPlaying) {
|
while (isActive && !isReleased.get() && player.isPlaying) {
|
||||||
// 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) {
|
||||||
@ -97,7 +98,7 @@ actual class VideoPlayer actual constructor(
|
|||||||
delay(50)
|
delay(50)
|
||||||
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
||||||
}
|
}
|
||||||
if (isActive && !isReleased) {
|
if (isActive && !isReleased.get()) {
|
||||||
onProgressUpdate(player.currentPosition.toLong(), TrackState.PAUSED)
|
onProgressUpdate(player.currentPosition.toLong(), TrackState.PAUSED)
|
||||||
}
|
}
|
||||||
onProgressUpdate(null, TrackState.PAUSED)
|
onProgressUpdate(null, TrackState.PAUSED)
|
||||||
@ -107,9 +108,11 @@ actual class VideoPlayer actual constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
if (isReleased || !videoPlaying.value) return
|
if (isReleased.get() || !videoPlaying.value) return
|
||||||
player.controls().stop()
|
playerThread.execute {
|
||||||
stopListener()
|
player.stop()
|
||||||
|
stopListener()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopListener() {
|
private fun stopListener() {
|
||||||
@ -133,45 +136,57 @@ actual class VideoPlayer actual constructor(
|
|||||||
if (progress.value == duration.value) {
|
if (progress.value == duration.value) {
|
||||||
progress.value = 0
|
progress.value = 0
|
||||||
}
|
}
|
||||||
videoPlaying.value = start(progress.value) { pro, _ ->
|
playerThread.execute {
|
||||||
if (pro != null) {
|
videoPlaying.value = start(progress.value) { pro, _ ->
|
||||||
progress.value = pro
|
if (pro != null) {
|
||||||
}
|
progress.value = pro
|
||||||
if ((pro == null || pro == duration.value) && duration.value != 0L) {
|
}
|
||||||
videoPlaying.value = false
|
if ((pro == null || pro == duration.value) && duration.value != 0L) {
|
||||||
if (pro == duration.value) {
|
videoPlaying.value = false
|
||||||
progress.value = if (resetOnEnd) 0 else duration.value
|
if (pro == duration.value) {
|
||||||
}/* else if (state == TrackState.STOPPED) {
|
progress.value = if (resetOnEnd) 0 else duration.value
|
||||||
|
}/* else if (state == TrackState.STOPPED) {
|
||||||
progress.value = 0 //
|
progress.value = 0 //
|
||||||
}*/
|
}*/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun enableSound(enable: Boolean): Boolean {
|
override fun enableSound(enable: Boolean): Boolean {
|
||||||
if (isReleased) return false
|
// Impossible to change volume for only one player. It changes for every player
|
||||||
if (soundEnabled.value == enable) return false
|
// https://github.com/caprica/vlcj/issues/985
|
||||||
|
return false
|
||||||
|
/*if (isReleased.get() || soundEnabled.value == enable) return false
|
||||||
soundEnabled.value = enable
|
soundEnabled.value = enable
|
||||||
player.audio().setVolume(if (enable) currentVolume else 0)
|
playerThread.execute {
|
||||||
return true
|
player.audio().isMute = !enable
|
||||||
|
}
|
||||||
|
return true*/
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun release(remove: Boolean) { withApi {
|
override fun release(remove: Boolean) {
|
||||||
if (isReleased) return@withApi
|
CoroutineScope(playerThread.asCoroutineDispatcher()).launch {
|
||||||
isReleased = true
|
if (isReleased.get()) return@launch
|
||||||
// TODO
|
isReleased.set(true)
|
||||||
/** [player.release] freezes thread for some reason. It happens periodically. So doing this we don't see the freeze, but it's still there */
|
if (player.isPlaying) {
|
||||||
if (player.isPlaying) player.stop()
|
player.stop()
|
||||||
CoroutineScope(Dispatchers.IO).launch { player.release() }
|
}
|
||||||
if (remove) {
|
if (usePool) {
|
||||||
VideoPlayerHolder.players.remove(uri to gallery)
|
putPlayer(mediaPlayerComponent)
|
||||||
|
} else {
|
||||||
|
player.release()
|
||||||
|
}
|
||||||
|
if (remove) {
|
||||||
|
VideoPlayerHolder.players.remove(uri to gallery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
|
|
||||||
private val MediaPlayer.currentPosition: Int
|
private val MediaPlayer.currentPosition: Int
|
||||||
get() = if (isReleased) 0 else max(0, player.status().time().toInt())
|
get() = if (isReleased.get()) 0 else max(0, status().time().toInt())
|
||||||
|
|
||||||
private suspend fun setPreviewAndDuration() {
|
private fun setPreviewAndDuration() {
|
||||||
// It freezes main thread, doing it in IO thread
|
// It freezes main thread, doing it in IO thread
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(defaultPreview, uri) }
|
val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(defaultPreview, uri) }
|
||||||
@ -182,35 +197,79 @@ actual class VideoPlayer actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeMediaPlayerComponent(): Component {
|
|
||||||
return if (desktopPlatform.isMac()) {
|
|
||||||
CallbackMediaPlayerComponent()
|
|
||||||
} else {
|
|
||||||
EmbeddedMediaPlayerComponent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Component.mediaPlayer() = when (this) {
|
|
||||||
is CallbackMediaPlayerComponent -> mediaPlayer()
|
|
||||||
is EmbeddedMediaPlayerComponent -> mediaPlayer()
|
|
||||||
else -> error("mediaPlayer() can only be called on vlcj player components")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?): VideoPlayerInterface.PreviewAndDuration {
|
private val usePool = false
|
||||||
val player = CallbackMediaPlayerComponent().mediaPlayer()
|
|
||||||
|
private fun Component.mediaPlayer() = when (this) {
|
||||||
|
is CallbackMediaPlayerComponent -> mediaPlayer()
|
||||||
|
is EmbeddedMediaPlayerComponent -> mediaPlayer()
|
||||||
|
else -> error("mediaPlayer() can only be called on vlcj player components")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeMediaPlayerComponent(): Component {
|
||||||
|
return if (desktopPlatform.isMac()) {
|
||||||
|
CallbackMediaPlayerComponent()
|
||||||
|
} else {
|
||||||
|
EmbeddedMediaPlayerComponent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?): VideoPlayerInterface.PreviewAndDuration = withContext(playerThread.asCoroutineDispatcher()) {
|
||||||
|
val mediaComponent = getOrCreateHelperPlayer()
|
||||||
|
val player = mediaComponent.mediaPlayer()
|
||||||
if (uri == null || !File(uri.rawPath).exists()) {
|
if (uri == null || !File(uri.rawPath).exists()) {
|
||||||
return VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L)
|
return@withContext VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L)
|
||||||
}
|
}
|
||||||
player.media().startPaused(uri.toString().replaceFirst("file:", "file://"))
|
player.media().startPaused(uri.toString().replaceFirst("file:", "file://"))
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
while (player.snapshots()?.get() == null && start + 5000 > System.currentTimeMillis()) {
|
var snap: BufferedImage? = null
|
||||||
|
while (snap == null && start + 5000 > System.currentTimeMillis()) {
|
||||||
|
snap = player.snapshots()?.get()
|
||||||
delay(10)
|
delay(10)
|
||||||
}
|
}
|
||||||
val preview = player.snapshots()?.get()?.toComposeImageBitmap()
|
val orientation = player.media().info().videoTracks().first().orientation()
|
||||||
|
val preview: ImageBitmap? = when (orientation) {
|
||||||
|
VideoOrientation.TOP_LEFT -> snap
|
||||||
|
VideoOrientation.TOP_RIGHT -> snap?.flip(false, true)
|
||||||
|
VideoOrientation.BOTTOM_LEFT -> snap?.flip(true, false)
|
||||||
|
VideoOrientation.BOTTOM_RIGHT -> snap?.rotate(180.0)
|
||||||
|
VideoOrientation.LEFT_TOP -> snap /* Transposed */
|
||||||
|
VideoOrientation.LEFT_BOTTOM -> snap?.rotate(-90.0)
|
||||||
|
VideoOrientation.RIGHT_TOP -> snap?.rotate(90.0)
|
||||||
|
VideoOrientation.RIGHT_BOTTOM -> snap /* Anti-transposed */
|
||||||
|
else -> snap
|
||||||
|
}?.toComposeImageBitmap()
|
||||||
val duration = player.duration.toLong()
|
val duration = player.duration.toLong()
|
||||||
CoroutineScope(Dispatchers.IO).launch { player.release() }
|
player.stop()
|
||||||
return VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration)
|
putHelperPlayer(mediaComponent)
|
||||||
|
return@withContext VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val playerThread = Executors.newSingleThreadExecutor()
|
||||||
|
private val playersPool: ArrayList<Component> = ArrayList()
|
||||||
|
private val helperPlayersPool: ArrayList<CallbackMediaPlayerComponent> = ArrayList()
|
||||||
|
|
||||||
|
private fun getOrCreatePlayer(): Component = playersPool.removeFirstOrNull() ?: createNew()
|
||||||
|
|
||||||
|
private fun createNew(): Component =
|
||||||
|
initializeMediaPlayerComponent().apply {
|
||||||
|
mediaPlayer().events().addMediaPlayerEventListener(object: MediaPlayerEventAdapter() {
|
||||||
|
override fun mediaPlayerReady(mediaPlayer: MediaPlayer?) {
|
||||||
|
playerThread.execute {
|
||||||
|
mediaPlayer?.audio()?.setVolume(100)
|
||||||
|
mediaPlayer?.audio()?.isMute = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopped(mediaPlayer: MediaPlayer?) {
|
||||||
|
//playerThread.execute { mediaPlayer().videoSurface().set(null) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putPlayer(player: Component) = playersPool.add(player)
|
||||||
|
|
||||||
|
private fun getOrCreateHelperPlayer(): CallbackMediaPlayerComponent = helperPlayersPool.removeFirstOrNull() ?: CallbackMediaPlayerComponent()
|
||||||
|
private fun putHelperPlayer(player: CallbackMediaPlayerComponent) = helperPlayersPool.add(player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,29 @@
|
|||||||
package chat.simplex.common.views.chat.item
|
package chat.simplex.common.views.chat.item
|
||||||
|
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import chat.simplex.common.platform.VideoPlayer
|
import chat.simplex.common.platform.VideoPlayer
|
||||||
|
import chat.simplex.common.platform.isPlaying
|
||||||
|
import chat.simplex.common.views.helpers.onRightClick
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {}
|
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {
|
||||||
|
Box {
|
||||||
|
SurfaceFromPlayer(player,
|
||||||
|
Modifier
|
||||||
|
.width(width)
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
onClick = { if (player.player.isPlaying) stop() else onClick() }
|
||||||
|
)
|
||||||
|
.onRightClick(onLongClick)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun LocalWindowWidth(): Dp {
|
actual fun LocalWindowWidth(): Dp {
|
||||||
|
@ -6,17 +6,15 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.awt.SwingPanel
|
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.simplexWindowState
|
|
||||||
import chat.simplex.common.views.helpers.getBitmapFromByteArray
|
import chat.simplex.common.views.helpers.getBitmapFromByteArray
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import kotlinx.coroutines.delay
|
import org.jetbrains.compose.videoplayer.SkiaBitmapVideoSurface
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -28,30 +26,40 @@ actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap:
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) {
|
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) {
|
||||||
// Workaround. Without changing size of the window the screen flashes a lot even if it's not being recomposed
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = simplexWindowState.windowState.size.width + 1.dp)
|
|
||||||
delay(50)
|
|
||||||
player.play(true)
|
|
||||||
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = simplexWindowState.windowState.size.width - 1.dp)
|
|
||||||
}
|
|
||||||
Box {
|
Box {
|
||||||
Box(Modifier.fillMaxSize().padding(bottom = 50.dp)) {
|
Box(Modifier.fillMaxSize().padding(bottom = 50.dp)) {
|
||||||
val factory = remember { { player.mediaPlayerComponent } }
|
SurfaceFromPlayer(player, modifier)
|
||||||
SwingPanel(
|
IconButton(onClick = close, Modifier.padding(top = 5.dp)) {
|
||||||
background = Color.Transparent,
|
Icon(painterResource(MR.images.ic_arrow_back_ios_new), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary)
|
||||||
modifier = Modifier,
|
}
|
||||||
factory = factory
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Controls(player, close)
|
Controls(player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BoxScope.Controls(player: VideoPlayer, close: () -> Unit) {
|
fun BoxScope.SurfaceFromPlayer(player: VideoPlayer, modifier: Modifier) {
|
||||||
|
val surface = remember {
|
||||||
|
SkiaBitmapVideoSurface().also {
|
||||||
|
player.player.videoSurface().set(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surface.bitmap.value?.let { bitmap ->
|
||||||
|
Image(
|
||||||
|
bitmap,
|
||||||
|
modifier = modifier.align(Alignment.Center),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
alignment = Alignment.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.Controls(player: VideoPlayer) {
|
||||||
val playing = remember(player) { player.videoPlaying }
|
val playing = remember(player) { player.videoPlaying }
|
||||||
val progress = remember(player) { player.progress }
|
val progress = remember(player) { player.progress }
|
||||||
val duration = remember(player) { player.duration }
|
val duration = remember(player) { player.duration }
|
||||||
@ -62,10 +70,7 @@ private fun BoxScope.Controls(player: VideoPlayer, close: () -> Unit) {
|
|||||||
Slider(
|
Slider(
|
||||||
value = progress.value.toFloat() / max(0.0001f, duration.value.toFloat()),
|
value = progress.value.toFloat() / max(0.0001f, duration.value.toFloat()),
|
||||||
onValueChange = { player.player.seekTo((it * duration.value).toInt()) },
|
onValueChange = { player.player.seekTo((it * duration.value).toInt()) },
|
||||||
modifier = Modifier.fillMaxWidth().weight(1f)
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
IconButton(onClick = close,) {
|
|
||||||
Icon(painterResource(MR.images.ic_close), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
layout: layouts/article.html
|
layout: layouts/article.html
|
||||||
title: "SimpleX Chat v5.3 released: desktop app, local file encryption and improved groups with directory service"
|
title: "SimpleX Chat v5.3 released: desktop app, local file encryption and improved groups with directory service"
|
||||||
date: 2023-09-25
|
date: 2023-09-25
|
||||||
image: /docs/images/simplex-desktop-light.png
|
image: images/simplex-desktop-light.png
|
||||||
imageWide: true
|
imageWide: true
|
||||||
previewBody: blog_previews/20230925.html
|
previewBody: blog_previews/20230925.html
|
||||||
permalink: "/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html"
|
permalink: "/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html"
|
||||||
@ -36,7 +36,7 @@ Also, we added 6 new interface languages: Arabic<sup>*</sup>, Bulgarian, Finnish
|
|||||||
|
|
||||||
## Multiplatform desktop app
|
## Multiplatform desktop app
|
||||||
|
|
||||||
<img src="/docs/images/simplex-desktop-light.png" width="640">
|
<img src="./images/simplex-desktop-light.png" width="640">
|
||||||
|
|
||||||
Thanks a lot to everybody who was testing the desktop app since July – it really helped to make it stable!
|
Thanks a lot to everybody who was testing the desktop app since July – it really helped to make it stable!
|
||||||
|
|
||||||
@ -50,6 +50,8 @@ Other limitations of the desktop app:
|
|||||||
- you cannot send voice messages.
|
- you cannot send voice messages.
|
||||||
- there is no support for calls yet.
|
- there is no support for calls yet.
|
||||||
|
|
||||||
|
You can download the desktop app for Linux and Mac via [downloads page](https://simplex.chat/downloads). Windows version will be available soon.
|
||||||
|
|
||||||
## Group directory service and other group improvements
|
## Group directory service and other group improvements
|
||||||
|
|
||||||
<img src="./images/20230925-directory.png" width="330" class="float-to-left">
|
<img src="./images/20230925-directory.png" width="330" class="float-to-left">
|
||||||
|
BIN
blog/images/simplex-desktop-light.png
Normal file
BIN
blog/images/simplex-desktop-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 545 KiB |
@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/simplexmq.git
|
location: https://github.com/simplex-chat/simplexmq.git
|
||||||
tag: 53c793d5590d3c781aa3fbf72993eee262c7aa83
|
tag: e7bd0fb31af4f04ce7ac6fdc2c81ef52ce918b48
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
@ -7,7 +7,7 @@ revision: 20.09.2023
|
|||||||
| Updated 20.09.2023 | Languages: EN |
|
| Updated 20.09.2023 | Languages: EN |
|
||||||
# Download SimpleX apps
|
# Download SimpleX apps
|
||||||
|
|
||||||
The latest version is v5.3.
|
The latest version is v5.3.1.
|
||||||
|
|
||||||
- [desktop](#desktop-app)
|
- [desktop](#desktop-app)
|
||||||
- [mobile](#mobile-apps)
|
- [mobile](#mobile-apps)
|
||||||
@ -19,24 +19,24 @@ The latest version is v5.3.
|
|||||||
|
|
||||||
Using the same profile as on mobile device is not yet supported – you need to create a separate profile to use desktop apps.
|
Using the same profile as on mobile device is not yet supported – you need to create a separate profile to use desktop apps.
|
||||||
|
|
||||||
**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-desktop-ubuntu-22_04-x86_64.deb).
|
**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-ubuntu-22_04-x86_64.deb).
|
||||||
|
|
||||||
**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-desktop-macos-aarch64.dmg) (Apple Silicon).
|
**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-macos-aarch64.dmg) (Apple Silicon).
|
||||||
|
|
||||||
**Windows**: coming soon.
|
**Windows**: coming soon.
|
||||||
|
|
||||||
## Mobile apps
|
## Mobile apps
|
||||||
|
|
||||||
**iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084) (v5.2.3), [TestFlight](https://testflight.apple.com/join/DWuT2LQu) (v5.3-beta.9).
|
**iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu).
|
||||||
|
|
||||||
**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-armv7a.apk).
|
**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-armv7a.apk).
|
||||||
|
|
||||||
## Terminal (console) app
|
## Terminal (console) app
|
||||||
|
|
||||||
See [Using terminal app](/docs/CLI.md).
|
See [Using terminal app](/docs/CLI.md).
|
||||||
|
|
||||||
**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-chat-ubuntu-22_04-x86-64).
|
**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-chat-ubuntu-22_04-x86-64).
|
||||||
|
|
||||||
**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-chat-macos-x86-64), aarch64 - [compile from source](./CLI.md#).
|
**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-chat-macos-x86-64), aarch64 - [compile from source](./CLI.md#).
|
||||||
|
|
||||||
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-chat-windows-x86-64).
|
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-chat-windows-x86-64).
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
OS=mac
|
OS=mac
|
||||||
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
|
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
|
||||||
if [ "$ARCH" == "arm64" ]; then
|
if [ "$ARCH" == "arm64" ]; then
|
||||||
@ -15,7 +17,7 @@ rm -rf $BUILD_DIR
|
|||||||
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/rts -optl-lHSrts_thr-ghc8.10.7 -optl-lffi"
|
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/rts -optl-lHSrts_thr-ghc8.10.7 -optl-lffi"
|
||||||
|
|
||||||
cd $BUILD_DIR/build
|
cd $BUILD_DIR/build
|
||||||
mkdir deps 2> /dev/null
|
mkdir deps 2> /dev/null || true
|
||||||
|
|
||||||
# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available
|
# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available
|
||||||
cp $GHC_LIBS_DIR/rts/libffi.dylib ./deps
|
cp $GHC_LIBS_DIR/rts/libffi.dylib ./deps
|
||||||
@ -38,7 +40,7 @@ function copy_deps() {
|
|||||||
|
|
||||||
cp $LIB ./deps
|
cp $LIB ./deps
|
||||||
if [[ "$NON_FINAL_RPATHS" == *"@loader_path/.."* ]]; then
|
if [[ "$NON_FINAL_RPATHS" == *"@loader_path/.."* ]]; then
|
||||||
# Need to point the lib to @loader_path instead
|
# Need to point the lib to @loader_path instead
|
||||||
install_name_tool -add_rpath @loader_path ./deps/`basename $LIB`
|
install_name_tool -add_rpath @loader_path ./deps/`basename $LIB`
|
||||||
fi
|
fi
|
||||||
#echo LIB $LIB
|
#echo LIB $LIB
|
||||||
@ -61,13 +63,6 @@ function copy_deps() {
|
|||||||
copy_deps $LIB
|
copy_deps $LIB
|
||||||
rm deps/`basename $LIB`
|
rm deps/`basename $LIB`
|
||||||
|
|
||||||
if [ -e deps/libHSdrct-*.$LIB_EXT ]; then
|
|
||||||
LIBCRYPTO_PATH=$(otool -l deps/libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
|
|
||||||
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSdrct*.$LIB_EXT
|
|
||||||
cp $LIBCRYPTO_PATH deps/libcrypto.1.1.$LIB_EXT
|
|
||||||
chmod 755 deps/libcrypto.1.1.$LIB_EXT
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd -
|
cd -
|
||||||
|
|
||||||
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
@ -77,4 +72,39 @@ rm -rf apps/multiplatform/desktop/build/cmake
|
|||||||
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
|
|
||||||
|
cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
|
|
||||||
|
LIBCRYPTO_PATH=$(otool -l deps/libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
|
||||||
|
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSdrct-*.$LIB_EXT
|
||||||
|
cp $LIBCRYPTO_PATH deps/libcrypto.1.1.$LIB_EXT
|
||||||
|
chmod 755 deps/libcrypto.1.1.$LIB_EXT
|
||||||
|
install_name_tool -id "libcrypto.1.1.$LIB_EXT" deps/libcrypto.1.1.$LIB_EXT
|
||||||
|
install_name_tool -id "libffi.8.$LIB_EXT" deps/libffi.$LIB_EXT
|
||||||
|
|
||||||
|
LIBCRYPTO_PATH=$(otool -l $LIB | grep libcrypto | cut -d' ' -f11)
|
||||||
|
if [ -n "$LIBCRYPTO_PATH" ]; then
|
||||||
|
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT $LIB
|
||||||
|
fi
|
||||||
|
|
||||||
|
LIBCRYPTO_PATH=$(otool -l deps/libHSsmplxmq*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
|
||||||
|
if [ -n "$LIBCRYPTO_PATH" ]; then
|
||||||
|
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSsmplxmq*.$LIB_EXT
|
||||||
|
fi
|
||||||
|
|
||||||
|
for lib in $(find . -type f -name "*.$LIB_EXT"); do
|
||||||
|
RPATHS=`otool -l $lib | grep -E "path /Users/|path /usr/local|path /opt/" | cut -d' ' -f11`
|
||||||
|
for RPATH in $RPATHS; do
|
||||||
|
install_name_tool -delete_rpath $RPATH $lib
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
LOCAL_DIRS=`for lib in $(find . -type f -name "*.$LIB_EXT"); do otool -l $lib | grep -E "/Users|/opt/|/usr/local" && echo $lib; done`
|
||||||
|
if [ -n "$LOCAL_DIRS" ]; then
|
||||||
|
echo These libs still point to local directories:
|
||||||
|
echo $LOCAL_DIRS
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd -
|
||||||
scripts/desktop/prepare-vlc-mac.sh
|
scripts/desktop/prepare-vlc-mac.sh
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."53c793d5590d3c781aa3fbf72993eee262c7aa83" = "0f0ldlgqwrapgfw5gnaj00xvb14c8nykyjr9fhy79h4r16g614x8";
|
"https://github.com/simplex-chat/simplexmq.git"."e7bd0fb31af4f04ce7ac6fdc2c81ef52ce918b48" = "0yzf8a3267c1iihhl1vx3rybzq0mwzkff57nrqhww1i8pmgva74i";
|
||||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||||
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
|
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
||||||
|
@ -79,7 +79,7 @@ import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentCl
|
|||||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
||||||
import Simplex.Messaging.Agent.Lock
|
import Simplex.Messaging.Agent.Lock
|
||||||
import Simplex.Messaging.Agent.Protocol
|
import Simplex.Messaging.Agent.Protocol
|
||||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection)
|
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection, closeSQLiteStore, openSQLiteStore)
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||||
@ -245,9 +245,13 @@ cfgServers = \case
|
|||||||
SPSMP -> smp
|
SPSMP -> smp
|
||||||
SPXFTP -> xftp
|
SPXFTP -> xftp
|
||||||
|
|
||||||
startChatController :: forall m. ChatMonad' m => Bool -> Bool -> Bool -> m (Async ())
|
startChatController :: forall m. ChatMonad' m => ChatCtrlCfg -> m (Async ())
|
||||||
startChatController subConns enableExpireCIs startXFTPWorkers = do
|
startChatController ChatCtrlCfg {subConns, enableExpireCIs, startXFTPWorkers, openDBWithKey} = do
|
||||||
asks smpAgent >>= resumeAgentClient
|
ChatController {chatStore, smpAgent} <- ask
|
||||||
|
forM_ openDBWithKey $ \(DBEncryptionKey dbKey) -> liftIO $ do
|
||||||
|
openSQLiteStore chatStore dbKey
|
||||||
|
openSQLiteStore (agentClientStore smpAgent) dbKey
|
||||||
|
resumeAgentClient smpAgent
|
||||||
unless subConns $
|
unless subConns $
|
||||||
chatWriteVar subscriptionMode SMOnlyCreate
|
chatWriteVar subscriptionMode SMOnlyCreate
|
||||||
users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers)
|
users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers)
|
||||||
@ -319,8 +323,8 @@ restoreCalls = do
|
|||||||
calls <- asks currentCalls
|
calls <- asks currentCalls
|
||||||
atomically $ writeTVar calls callsMap
|
atomically $ writeTVar calls callsMap
|
||||||
|
|
||||||
stopChatController :: forall m. MonadUnliftIO m => ChatController -> m ()
|
stopChatController :: forall m. MonadUnliftIO m => ChatController -> Bool -> m ()
|
||||||
stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, expireCIFlags} = do
|
stopChatController ChatController {chatStore, smpAgent, agentAsync = s, sndFiles, rcvFiles, expireCIFlags} closeStore = do
|
||||||
disconnectAgentClient smpAgent
|
disconnectAgentClient smpAgent
|
||||||
readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2)
|
readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2)
|
||||||
closeFiles sndFiles
|
closeFiles sndFiles
|
||||||
@ -329,6 +333,9 @@ stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles,
|
|||||||
keys <- M.keys <$> readTVar expireCIFlags
|
keys <- M.keys <$> readTVar expireCIFlags
|
||||||
forM_ keys $ \k -> TM.insert k False expireCIFlags
|
forM_ keys $ \k -> TM.insert k False expireCIFlags
|
||||||
writeTVar s Nothing
|
writeTVar s Nothing
|
||||||
|
when closeStore $ liftIO $ do
|
||||||
|
closeSQLiteStore chatStore
|
||||||
|
closeSQLiteStore $ agentClientStore smpAgent
|
||||||
where
|
where
|
||||||
closeFiles :: TVar (Map Int64 Handle) -> m ()
|
closeFiles :: TVar (Map Int64 Handle) -> m ()
|
||||||
closeFiles files = do
|
closeFiles files = do
|
||||||
@ -458,12 +465,12 @@ processChatCommand = \case
|
|||||||
checkDeleteChatUser user'
|
checkDeleteChatUser user'
|
||||||
withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues
|
withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues
|
||||||
DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_
|
DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_
|
||||||
StartChat subConns enableExpireCIs startXFTPWorkers -> withUser' $ \_ ->
|
APIStartChat cfg -> withUser' $ \_ ->
|
||||||
asks agentAsync >>= readTVarIO >>= \case
|
asks agentAsync >>= readTVarIO >>= \case
|
||||||
Just _ -> pure CRChatRunning
|
Just _ -> pure CRChatRunning
|
||||||
_ -> checkStoreNotChanged $ startChatController subConns enableExpireCIs startXFTPWorkers $> CRChatStarted
|
_ -> checkStoreNotChanged $ startChatController cfg $> CRChatStarted
|
||||||
APIStopChat -> do
|
APIStopChat closeStore -> do
|
||||||
ask >>= stopChatController
|
ask >>= (`stopChatController` closeStore)
|
||||||
pure CRChatStopped
|
pure CRChatStopped
|
||||||
APIActivateChat -> withUser $ \_ -> do
|
APIActivateChat -> withUser $ \_ -> do
|
||||||
restoreCalls
|
restoreCalls
|
||||||
@ -5370,9 +5377,9 @@ chatCommandP =
|
|||||||
"/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)),
|
"/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)),
|
||||||
"/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)),
|
"/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)),
|
||||||
("/user" <|> "/u") $> ShowActiveUser,
|
("/user" <|> "/u") $> ShowActiveUser,
|
||||||
"/_start subscribe=" *> (StartChat <$> onOffP <* " expire=" <*> onOffP <* " xftp=" <*> onOffP),
|
"/_start" *> (APIStartChat <$> ((A.space *> jsonP) <|> chatCtrlCfgP)),
|
||||||
"/_start" $> StartChat True True True,
|
"/_stop close" $> APIStopChat {closeStore = True},
|
||||||
"/_stop" $> APIStopChat,
|
"/_stop" $> APIStopChat False,
|
||||||
"/_app activate" $> APIActivateChat,
|
"/_app activate" $> APIActivateChat,
|
||||||
"/_app suspend " *> (APISuspendChat <$> A.decimal),
|
"/_app suspend " *> (APISuspendChat <$> A.decimal),
|
||||||
"/_resubscribe all" $> ResubscribeAllConnections,
|
"/_resubscribe all" $> ResubscribeAllConnections,
|
||||||
@ -5600,6 +5607,12 @@ chatCommandP =
|
|||||||
]
|
]
|
||||||
where
|
where
|
||||||
choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput)
|
choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput)
|
||||||
|
chatCtrlCfgP = do
|
||||||
|
subConns <- (" subscribe=" *> onOffP) <|> pure True
|
||||||
|
enableExpireCIs <- (" expire=" *> onOffP) <|> pure True
|
||||||
|
startXFTPWorkers <- (" xftp=" *> onOffP) <|> pure True
|
||||||
|
openDBWithKey <- optional $ " key=" *> dbKeyP
|
||||||
|
pure ChatCtrlCfg {subConns, enableExpireCIs, startXFTPWorkers, openDBWithKey}
|
||||||
incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False
|
incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False
|
||||||
incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False
|
incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False
|
||||||
imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,")
|
imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,")
|
||||||
|
@ -123,7 +123,7 @@ sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = D
|
|||||||
checkFile `with` fs
|
checkFile `with` fs
|
||||||
backup `with` fs
|
backup `with` fs
|
||||||
(export chatDb chatEncrypted >> export agentDb agentEncrypted)
|
(export chatDb chatEncrypted >> export agentDb agentEncrypted)
|
||||||
`catchChatError` \e -> (restore `with` fs) >> throwError e
|
`catchChatError` \e -> tryChatError (restore `with` fs) >> throwError e
|
||||||
where
|
where
|
||||||
action `with` StorageFiles {chatDb, agentDb} = action chatDb >> action agentDb
|
action `with` StorageFiles {chatDb, agentDb} = action chatDb >> action agentDb
|
||||||
backup f = copyFile f (f <> ".bak")
|
backup f = copyFile f (f <> ".bak")
|
||||||
|
@ -221,8 +221,8 @@ data ChatCommand
|
|||||||
| UnmuteUser
|
| UnmuteUser
|
||||||
| APIDeleteUser UserId Bool (Maybe UserPwd)
|
| APIDeleteUser UserId Bool (Maybe UserPwd)
|
||||||
| DeleteUser UserName Bool (Maybe UserPwd)
|
| DeleteUser UserName Bool (Maybe UserPwd)
|
||||||
| StartChat {subscribeConnections :: Bool, enableExpireChatItems :: Bool, startXFTPWorkers :: Bool}
|
| APIStartChat ChatCtrlCfg
|
||||||
| APIStopChat
|
| APIStopChat {closeStore :: Bool}
|
||||||
| APIActivateChat
|
| APIActivateChat
|
||||||
| APISuspendChat {suspendTimeout :: Int}
|
| APISuspendChat {suspendTimeout :: Int}
|
||||||
| ResubscribeAllConnections
|
| ResubscribeAllConnections
|
||||||
@ -620,6 +620,17 @@ instance ToJSON ChatResponse where
|
|||||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR"
|
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR"
|
||||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR"
|
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR"
|
||||||
|
|
||||||
|
data ChatCtrlCfg = ChatCtrlCfg
|
||||||
|
{ subConns :: Bool,
|
||||||
|
enableExpireCIs :: Bool,
|
||||||
|
startXFTPWorkers :: Bool,
|
||||||
|
openDBWithKey :: Maybe DBEncryptionKey
|
||||||
|
}
|
||||||
|
deriving (Show, Generic, FromJSON)
|
||||||
|
|
||||||
|
defChatCtrlCfg :: ChatCtrlCfg
|
||||||
|
defChatCtrlCfg = ChatCtrlCfg True True True Nothing
|
||||||
|
|
||||||
newtype UserPwd = UserPwd {unUserPwd :: Text}
|
newtype UserPwd = UserPwd {unUserPwd :: Text}
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController
|
|||||||
runSimplexChat ChatOpts {maintenance} u cc chat
|
runSimplexChat ChatOpts {maintenance} u cc chat
|
||||||
| maintenance = wait =<< async (chat u cc)
|
| maintenance = wait =<< async (chat u cc)
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
a1 <- runReaderT (startChatController True True True) cc
|
a1 <- runReaderT (startChatController defChatCtrlCfg) cc
|
||||||
a2 <- async $ chat u cc
|
a2 <- async $ chat u cc
|
||||||
waitEither_ a1 a2
|
waitEither_ a1 a2
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ extra-deps:
|
|||||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||||
# - ../simplexmq
|
# - ../simplexmq
|
||||||
- github: simplex-chat/simplexmq
|
- github: simplex-chat/simplexmq
|
||||||
commit: 53c793d5590d3c781aa3fbf72993eee262c7aa83
|
commit: e7bd0fb31af4f04ce7ac6fdc2c81ef52ce918b48
|
||||||
- github: kazu-yamamoto/http2
|
- github: kazu-yamamoto/http2
|
||||||
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||||
# - ../direct-sqlcipher
|
# - ../direct-sqlcipher
|
||||||
|
@ -168,7 +168,7 @@ startTestChat_ db cfg opts user = do
|
|||||||
|
|
||||||
stopTestChat :: TestCC -> IO ()
|
stopTestChat :: TestCC -> IO ()
|
||||||
stopTestChat TestCC {chatController = cc, chatAsync, termAsync} = do
|
stopTestChat TestCC {chatController = cc, chatAsync, termAsync} = do
|
||||||
stopChatController cc
|
stopChatController cc False
|
||||||
uninterruptibleCancel termAsync
|
uninterruptibleCancel termAsync
|
||||||
uninterruptibleCancel chatAsync
|
uninterruptibleCancel chatAsync
|
||||||
threadDelay 200000
|
threadDelay 200000
|
||||||
|
@ -953,10 +953,15 @@ testDatabaseEncryption tmp = do
|
|||||||
alice ##> "/_start"
|
alice ##> "/_start"
|
||||||
alice <## "chat started"
|
alice <## "chat started"
|
||||||
testChatWorking alice bob
|
testChatWorking alice bob
|
||||||
alice ##> "/_stop"
|
alice ##> "/_stop close"
|
||||||
alice <## "chat stopped"
|
alice <## "chat stopped"
|
||||||
alice ##> "/db key wrongkey nextkey"
|
alice ##> "/db key wrongkey nextkey"
|
||||||
alice <## "error encrypting database: wrong passphrase or invalid database file"
|
alice <## "error encrypting database: wrong passphrase or invalid database file"
|
||||||
|
alice ##> "/_start key=mykey"
|
||||||
|
alice <## "chat started"
|
||||||
|
testChatWorking alice bob
|
||||||
|
alice ##> "/_stop close"
|
||||||
|
alice <## "chat stopped"
|
||||||
alice ##> "/db key mykey nextkey"
|
alice ##> "/db key mykey nextkey"
|
||||||
alice <## "ok"
|
alice <## "ok"
|
||||||
alice ##> "/_db encryption {\"currentKey\":\"nextkey\",\"newKey\":\"anotherkey\"}"
|
alice ##> "/_db encryption {\"currentKey\":\"nextkey\",\"newKey\":\"anotherkey\"}"
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
<ul class="mb-[12px]">
|
<ul class="mb-[12px]">
|
||||||
<li>new desktop app! 💻</li>
|
<li>new desktop app! 💻</li>
|
||||||
<li>encrypt locally storead files & media</li>
|
<li>encrypt locally stored files & media</li>
|
||||||
<li>directory service and other group improvements</li>
|
<li>directory service and other group improvements</li>
|
||||||
<li>simplified incognito mode</li>
|
<li>simplified incognito mode</li>
|
||||||
<li>better app responsiveness and stability, and 40% reduced memory usage.</li>
|
<li>better app responsiveness, stability and 40% reduced memory usage.</li>
|
||||||
<li>new privacy settings: show last messages & save draft.</li>
|
<li>new privacy settings: show last messages & save draft.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user