Merge branch 'master' into master-ghc8107

This commit is contained in:
Evgeny Poberezkin
2023-09-22 13:46:50 +01:00
50 changed files with 936 additions and 303 deletions
@@ -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)
}
@@ -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
@@ -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()
}
}
@@ -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) {
@@ -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)
@@ -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(
@@ -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,
)
}
@@ -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)
@@ -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
@@ -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)
@@ -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 {