mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-06-03 23:31:49 +00:00
Merge branch 'master' into master-ghc8107
This commit is contained in:
+19
-4
@@ -7,13 +7,12 @@ import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.ComposeState
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.common.platform.AudioPlayer
|
||||
import chat.simplex.common.platform.chatController
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
@@ -1417,8 +1416,7 @@ data class ChatItem (
|
||||
val encryptedFile: Boolean? = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null
|
||||
|
||||
val encryptLocalFile: Boolean
|
||||
get() = file?.fileProtocol == FileProtocol.XFTP &&
|
||||
content.msgContent !is MsgContent.MCVideo &&
|
||||
get() = content.msgContent !is MsgContent.MCVideo &&
|
||||
chatController.appPrefs.privacyEncryptLocalFiles.get()
|
||||
|
||||
val memberDisplayName: String? get() =
|
||||
@@ -2113,6 +2111,23 @@ data class CryptoFile(
|
||||
val isAbsolutePath: Boolean
|
||||
get() = File(filePath).isAbsolute
|
||||
|
||||
@Transient
|
||||
private var tmpFile: File? = null
|
||||
|
||||
fun createTmpFileIfNeeded(): File {
|
||||
if (tmpFile == null) {
|
||||
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
|
||||
tmpFile.deleteOnExit()
|
||||
ChatModel.filesToDelete.add(tmpFile)
|
||||
this.tmpFile = tmpFile
|
||||
}
|
||||
return tmpFile!!
|
||||
}
|
||||
|
||||
fun deleteTmpFile() {
|
||||
tmpFile?.delete()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun plain(f: String): CryptoFile = CryptoFile(f, null)
|
||||
}
|
||||
|
||||
+2
-2
@@ -2392,7 +2392,7 @@ data class NetCfg(
|
||||
sessionMode = TransportSessionMode.User,
|
||||
tcpConnectTimeout = 15_000_000,
|
||||
tcpTimeout = 10_000_000,
|
||||
tcpTimeoutPerKb = 20_000,
|
||||
tcpTimeoutPerKb = 30_000,
|
||||
tcpKeepAlive = KeepAliveOpts.defaults,
|
||||
smpPingInterval = 1200_000_000,
|
||||
smpPingCount = 3
|
||||
@@ -2406,7 +2406,7 @@ data class NetCfg(
|
||||
sessionMode = TransportSessionMode.User,
|
||||
tcpConnectTimeout = 30_000_000,
|
||||
tcpTimeout = 20_000_000,
|
||||
tcpTimeoutPerKb = 40_000,
|
||||
tcpTimeoutPerKb = 60_000,
|
||||
tcpKeepAlive = KeepAliveOpts.defaults,
|
||||
smpPingInterval = 1200_000_000,
|
||||
smpPingCount = 3
|
||||
|
||||
+42
-13
@@ -7,6 +7,8 @@ import java.net.URI
|
||||
interface VideoPlayerInterface {
|
||||
data class PreviewAndDuration(val preview: ImageBitmap?, val duration: Long?, val timestamp: Long)
|
||||
|
||||
val uri: URI
|
||||
val gallery: Boolean
|
||||
val soundEnabled: MutableState<Boolean>
|
||||
val brokenVideo: MutableState<Boolean>
|
||||
val videoPlaying: MutableState<Boolean>
|
||||
@@ -20,18 +22,45 @@ interface VideoPlayerInterface {
|
||||
fun release(remove: Boolean)
|
||||
}
|
||||
|
||||
expect class VideoPlayer: VideoPlayerInterface {
|
||||
companion object {
|
||||
fun getOrCreate(
|
||||
uri: URI,
|
||||
gallery: Boolean,
|
||||
defaultPreview: ImageBitmap,
|
||||
defaultDuration: Long,
|
||||
soundEnabled: Boolean
|
||||
): VideoPlayer
|
||||
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean
|
||||
fun release(uri: URI, gallery: Boolean, remove: Boolean)
|
||||
fun stopAll()
|
||||
fun releaseAll()
|
||||
expect class VideoPlayer(
|
||||
uri: URI,
|
||||
gallery: Boolean,
|
||||
defaultPreview: ImageBitmap,
|
||||
defaultDuration: Long,
|
||||
soundEnabled: Boolean
|
||||
): VideoPlayerInterface
|
||||
|
||||
object VideoPlayerHolder {
|
||||
val players: MutableMap<Pair<URI, Boolean>, VideoPlayer> = mutableMapOf()
|
||||
val previewsAndDurations: MutableMap<URI, VideoPlayerInterface.PreviewAndDuration> = mutableMapOf()
|
||||
|
||||
fun getOrCreate(
|
||||
uri: URI,
|
||||
gallery: Boolean,
|
||||
defaultPreview: ImageBitmap,
|
||||
defaultDuration: Long,
|
||||
soundEnabled: Boolean
|
||||
): VideoPlayer =
|
||||
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) }
|
||||
|
||||
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean =
|
||||
player(fileName, gallery)?.enableSound(enable) == true
|
||||
|
||||
private fun player(fileName: String?, gallery: Boolean): VideoPlayer? {
|
||||
fileName ?: return null
|
||||
return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery }
|
||||
}
|
||||
|
||||
fun release(uri: URI, gallery: Boolean, remove: Boolean) =
|
||||
player(uri.path, gallery)?.release(remove).run { }
|
||||
|
||||
fun stopAll() {
|
||||
players.values.forEach { it.stop() }
|
||||
}
|
||||
|
||||
fun releaseAll() {
|
||||
players.values.forEach { it.release(false) }
|
||||
players.clear()
|
||||
previewsAndDurations.clear()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -449,7 +449,7 @@ fun ChatLayout(
|
||||
val images = groups[true] ?: emptyList()
|
||||
val files = groups[false] ?: emptyList()
|
||||
if (images.isNotEmpty()) {
|
||||
composeState.processPickedMedia(images, null)
|
||||
CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(images, null) }
|
||||
} else if (files.isNotEmpty()) {
|
||||
composeState.processPickedFile(uris.first(), null)
|
||||
}
|
||||
@@ -459,7 +459,7 @@ fun ChatLayout(
|
||||
tmpFile.deleteOnExit()
|
||||
chatModel.filesToDelete.add(tmpFile)
|
||||
val uri = tmpFile.toURI()
|
||||
composeState.processPickedMedia(listOf(uri), null)
|
||||
CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) }
|
||||
},
|
||||
onText = {
|
||||
// Need to parse HTML in order to correctly display the content
|
||||
@@ -737,7 +737,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
DisposableEffectOnGone(
|
||||
whenGone = {
|
||||
VideoPlayer.releaseAll()
|
||||
VideoPlayerHolder.releaseAll()
|
||||
}
|
||||
)
|
||||
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
||||
|
||||
+2
-2
@@ -176,7 +176,7 @@ fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) {
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text: String?) {
|
||||
suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text: String?) {
|
||||
val content = ArrayList<UploadContent>()
|
||||
val imagesPreview = ArrayList<String>()
|
||||
uris.forEach { uri ->
|
||||
@@ -237,7 +237,7 @@ fun ComposeView(
|
||||
val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) }
|
||||
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
|
||||
|
||||
AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile, composeState::processPickedMedia)
|
||||
AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile) { uris, text -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(uris, text) } }
|
||||
|
||||
fun isSimplexLink(link: String): Boolean =
|
||||
link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true)
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ fun CIFileView(
|
||||
when (file.fileStatus) {
|
||||
is CIFileStatus.RcvInvitation -> {
|
||||
if (fileSizeValid()) {
|
||||
val encrypted = file.fileProtocol == FileProtocol.XFTP && chatController.appPrefs.privacyEncryptLocalFiles.get()
|
||||
val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()
|
||||
receiveFile(file.fileId, encrypted)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
|
||||
+13
-9
@@ -50,7 +50,7 @@ fun CIVideoView(
|
||||
})
|
||||
} else {
|
||||
Box {
|
||||
ImageView(preview, showMenu, onClick = {
|
||||
VideoPreviewImageView(preview, onClick = {
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation ->
|
||||
@@ -75,7 +75,10 @@ fun CIVideoView(
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onLongClick = {
|
||||
showMenu.value = true
|
||||
})
|
||||
if (file != null) {
|
||||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
||||
}
|
||||
@@ -90,7 +93,7 @@ fun CIVideoView(
|
||||
|
||||
@Composable
|
||||
private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
|
||||
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true) }
|
||||
val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, false, defaultPreview, defaultDuration, true) }
|
||||
val videoPlaying = remember(uri.path) { player.videoPlaying }
|
||||
val progress = remember(uri.path) { player.progress }
|
||||
val duration = remember(uri.path) { player.duration }
|
||||
@@ -111,6 +114,7 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
|
||||
stop()
|
||||
}
|
||||
}
|
||||
val onLongClick = { showMenu.value = true }
|
||||
Box {
|
||||
val windowWidth = LocalWindowWidth()
|
||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
|
||||
@@ -118,12 +122,12 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
|
||||
player,
|
||||
width,
|
||||
onClick = onClick,
|
||||
onLongClick = { showMenu.value = true },
|
||||
onLongClick = onLongClick,
|
||||
stop
|
||||
)
|
||||
if (showPreview.value) {
|
||||
ImageView(preview, showMenu, onClick)
|
||||
PlayButton(brokenVideo, onLongClick = { showMenu.value = true }, play)
|
||||
VideoPreviewImageView(preview, onClick, onLongClick)
|
||||
PlayButton(brokenVideo, onLongClick = onLongClick, if (appPlatform.isAndroid) play else onClick)
|
||||
}
|
||||
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
|
||||
}
|
||||
@@ -201,7 +205,7 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
|
||||
fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) {
|
||||
val windowWidth = LocalWindowWidth()
|
||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
|
||||
Image(
|
||||
@@ -210,10 +214,10 @@ private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onC
|
||||
modifier = Modifier
|
||||
.width(width)
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick
|
||||
)
|
||||
.onRightClick { showMenu.value = true },
|
||||
.onRightClick(onLongClick),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
|
||||
+18
-8
@@ -46,9 +46,11 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
val scope = rememberCoroutineScope()
|
||||
val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
|
||||
DisposableEffectOnGone(
|
||||
whenGone = { playersToRelease.forEach { VideoPlayer.release(it, true, true) } }
|
||||
whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } }
|
||||
)
|
||||
HorizontalPager(pageCount = remember { provider.totalMediaSize }.value, state = pagerState) { index ->
|
||||
|
||||
@Composable
|
||||
fun Content(index: Int) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -127,7 +129,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
FullScreenImageView(modifier, data, imageBitmap)
|
||||
} else if (media is ProviderMedia.Video) {
|
||||
val preview = remember(media.uri.path) { base64ToBitmap(media.preview) }
|
||||
VideoView(modifier, media.uri, preview, index == settledCurrentPage)
|
||||
VideoView(modifier, media.uri, preview, index == settledCurrentPage, close)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { playersToRelease.add(media.uri) }
|
||||
}
|
||||
@@ -135,14 +137,19 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
}
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
HorizontalPager(pageCount = remember { provider.totalMediaSize }.value, state = pagerState) { index -> Content(index) }
|
||||
} else {
|
||||
Content(pagerState.currentPage)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap)
|
||||
|
||||
@Composable
|
||||
private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean) {
|
||||
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true) }
|
||||
private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean, close: () -> Unit) {
|
||||
val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, 0L, true) }
|
||||
val isCurrentPage = rememberUpdatedState(currentPage)
|
||||
val play = {
|
||||
player.play(true)
|
||||
@@ -154,13 +161,16 @@ private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap,
|
||||
player.enableSound(true)
|
||||
snapshotFlow { isCurrentPage.value }
|
||||
.distinctUntilChanged()
|
||||
.collect { if (it) play() else stop() }
|
||||
.collect {
|
||||
// Do not autoplay on desktop because it needs workaround
|
||||
if (it && appPlatform.isAndroid) play() else if (!it) stop()
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
FullScreenVideoView(player, modifier)
|
||||
FullScreenVideoView(player, modifier, close)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier)
|
||||
expect fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit)
|
||||
|
||||
+2
@@ -66,6 +66,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
if (chatModel.chatId.value != null) {
|
||||
ModalManager.end.closeModalsExceptFirst()
|
||||
}
|
||||
AudioPlayer.stop()
|
||||
VideoPlayerHolder.stopAll()
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
|
||||
+1
-1
@@ -267,7 +267,7 @@ fun getMaxFileSize(fileProtocol: FileProtocol): Long {
|
||||
}
|
||||
}
|
||||
|
||||
expect fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true): VideoPlayerInterface.PreviewAndDuration
|
||||
expect suspend fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true): VideoPlayerInterface.PreviewAndDuration
|
||||
|
||||
fun Color.darker(factor: Float = 0.1f): Color =
|
||||
Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha)
|
||||
|
||||
+2
-1
@@ -164,9 +164,10 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
)
|
||||
}
|
||||
SectionItemView {
|
||||
// can't be higher than 130ms to avoid overflow on 32bit systems
|
||||
TimeoutSettingRow(
|
||||
stringResource(MR.strings.network_option_protocol_timeout_per_kb), networkTCPTimeoutPerKb,
|
||||
listOf(10_000, 20_000, 40_000, 75_000, 100_000), secondsLabel
|
||||
listOf(15_000, 30_000, 60_000, 90_000, 120_000), secondsLabel
|
||||
)
|
||||
}
|
||||
SectionItemView {
|
||||
|
||||
Reference in New Issue
Block a user