android, desktop: blur for media (#4508)

* android, desktop: blur for media

* change

* new option and applied blur to other elements

* new line

* added to migration

* long click handling

* hover on desktop

* changes

* change

* showDownloadButton function

* file rename

* don't blur when menu is visible

* rename
This commit is contained in:
Stanislav Dmitrenko
2024-07-26 15:29:13 +07:00
committed by GitHub
parent a53333be20
commit 032c5d3a5b
12 changed files with 216 additions and 34 deletions

View File

@@ -28,3 +28,5 @@ actual fun Modifier.desktopOnExternalDrag(
actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this
actual fun Modifier.desktopPointerHoverIconHand(): Modifier = this
actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier = Modifier

View File

@@ -116,6 +116,7 @@ class AppPreferences {
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
@@ -328,6 +329,7 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius"
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
@@ -5998,6 +6000,7 @@ data class AppSettings(
var privacyShowChatPreviews: Boolean? = null,
var privacySaveLastDraft: Boolean? = null,
var privacyProtectScreen: Boolean? = null,
var privacyMediaBlurRadius: Int? = null,
var notificationMode: AppSettingsNotificationMode? = null,
var notificationPreviewMode: AppSettingsNotificationPreviewMode? = null,
var webrtcPolicyRelay: Boolean? = null,
@@ -6027,6 +6030,7 @@ data class AppSettings(
if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews }
if (privacySaveLastDraft != def.privacySaveLastDraft) { empty.privacySaveLastDraft = privacySaveLastDraft }
if (privacyProtectScreen != def.privacyProtectScreen) { empty.privacyProtectScreen = privacyProtectScreen }
if (privacyMediaBlurRadius != def.privacyMediaBlurRadius) { empty.privacyMediaBlurRadius = privacyMediaBlurRadius }
if (notificationMode != def.notificationMode) { empty.notificationMode = notificationMode }
if (notificationPreviewMode != def.notificationPreviewMode) { empty.notificationPreviewMode = notificationPreviewMode }
if (webrtcPolicyRelay != def.webrtcPolicyRelay) { empty.webrtcPolicyRelay = webrtcPolicyRelay }
@@ -6064,6 +6068,7 @@ data class AppSettings(
privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) }
privacySaveLastDraft?.let { def.privacySaveLastDraft.set(it) }
privacyProtectScreen?.let { def.privacyProtectScreen.set(it) }
privacyMediaBlurRadius?.let { def.privacyMediaBlurRadius.set(it) }
notificationMode?.let { def.notificationsMode.set(it.toNotificationsMode()) }
notificationPreviewMode?.let { def.notificationPreviewMode.set(it.toNotificationPreviewMode().name) }
webrtcPolicyRelay?.let { def.webrtcPolicyRelay.set(it) }
@@ -6094,6 +6099,7 @@ data class AppSettings(
privacyShowChatPreviews = true,
privacySaveLastDraft = true,
privacyProtectScreen = false,
privacyMediaBlurRadius = 0,
notificationMode = AppSettingsNotificationMode.INSTANT,
notificationPreviewMode = AppSettingsNotificationPreviewMode.MESSAGE,
webrtcPolicyRelay = true,
@@ -6125,6 +6131,7 @@ data class AppSettings(
privacyShowChatPreviews = def.privacyShowChatPreviews.get(),
privacySaveLastDraft = def.privacySaveLastDraft.get(),
privacyProtectScreen = def.privacyProtectScreen.get(),
privacyMediaBlurRadius = def.privacyMediaBlurRadius.get(),
notificationMode = AppSettingsNotificationMode.from(def.notificationsMode.get()),
notificationPreviewMode = AppSettingsNotificationPreviewMode.from(NotificationPreviewMode.valueOf(def.notificationPreviewMode.get()!!)),
webrtcPolicyRelay = def.webrtcPolicyRelay.get(),

View File

@@ -1,8 +1,17 @@
package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.views.helpers.KeyChangeEffect
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filter
import java.io.File
expect fun Modifier.navigationBarsWithImePadding(): Modifier
@@ -25,3 +34,68 @@ expect fun Modifier.desktopOnExternalDrag(
expect fun Modifier.onRightClick(action: () -> Unit): Modifier
expect fun Modifier.desktopPointerHoverIconHand(): Modifier
expect fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier
@Composable
fun Modifier.desktopModifyBlurredState(enabled: Boolean, blurred: MutableState<Boolean>, showMenu: State<Boolean>,): Modifier {
val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state }
if (appPlatform.isDesktop) {
KeyChangeEffect(blurRadius.value) {
blurred.value = enabled && blurRadius.value > 0
}
}
return if (appPlatform.isDesktop && enabled && blurRadius.value > 0 && !showMenu.value) {
var job: Job = remember { Job() }
LaunchedEffect(Unit) {
// The approach here is to allow menu to show up and to not blur the view. When menu is shown and mouse is hovering,
// unhovered action is still received, but we don't need to handle it until menu closes. When it closes, it takes one frame to catch a
// hover action again and if:
// 1. mouse is still on the view, the hover action will cancel this coroutine and the view will stay unblurred
// 2. mouse is not on the view, the view will become blurred after 100 ms
job = launch {
delay(100)
blurred.value = true
}
}
this then Modifier.desktopOnHovered { hovered ->
job.cancel()
blurred.value = !hovered && !showMenu.value
}
} else {
this
}
}
@Composable
fun Modifier.privacyBlur(
enabled: Boolean,
blurred: MutableState<Boolean> = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) },
scrollState: State<Boolean>,
onLongClick: () -> Unit = {}
): Modifier {
val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state }
return if (enabled && blurred.value) {
this then Modifier.blur(
radiusX = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp,
radiusY = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp,
edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(0.dp))
)
.combinedClickable(
onLongClick = onLongClick,
onClick = {
blurred.value = false
}
)
} else if (enabled && blurRadius.value > 0 && appPlatform.isAndroid) {
LaunchedEffect(Unit) {
snapshotFlow { scrollState.value }
.filter { it }
.filter { !blurred.value }
.collect { blurred.value = true }
}
this
} else {
this
}
}

View File

@@ -1113,6 +1113,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
}
FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
LaunchedEffect(Unit) {
snapshotFlow { listState.isScrollInProgress }
.collect {
chatViewScrollState.value = it
}
}
}
@Composable
@@ -1326,6 +1332,8 @@ private fun TopEndFloatingButton(
}
}
val chatViewScrollState = MutableStateFlow(false)
private fun bottomEndFloatingButton(
unreadCount: Int,
showButtonWithCounter: Boolean,

View File

@@ -1,6 +1,8 @@
package chat.simplex.common.views.chat.item
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.HoverInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -17,8 +19,10 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
import chat.simplex.common.views.chat.chatViewScrollState
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.runBlocking
@@ -32,6 +36,7 @@ fun CIImageView(
smallView: Boolean,
receiveFile: (Long) -> Unit
) {
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
@Composable
fun progressIndicator() {
CircularProgressIndicator(
@@ -105,7 +110,8 @@ fun CIImageView(
onLongClick = { showMenu.value = true },
onClick = onClick
)
.onRightClick { showMenu.value = true },
.onRightClick { showMenu.value = true }
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
)
}
@@ -128,7 +134,8 @@ fun CIImageView(
onLongClick = { showMenu.value = true },
onClick = onClick
)
.onRightClick { showMenu.value = true },
.onRightClick { showMenu.value = true }
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
)
} else {
@@ -138,7 +145,8 @@ fun CIImageView(
onLongClick = { showMenu.value = true },
onClick = {}
)
.onRightClick { showMenu.value = true },
.onRightClick { showMenu.value = true }
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
contentAlignment = Alignment.Center
) {
imageView(base64ToBitmap(image), onClick = {
@@ -174,7 +182,8 @@ fun CIImageView(
}
Box(
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID)
.desktopModifyBlurredState(!smallView, blurred, showMenu),
contentAlignment = Alignment.TopEnd
) {
val res: MutableState<Triple<ImageBitmap, ByteArray, String>?> = remember {
@@ -256,9 +265,10 @@ fun CIImageView(
}
})
}
if (!smallView) {
// Do not show download icon when the view is blurred
if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) {
loadingIndicator()
} else if (file?.showStatusIconInSmallView == true) {
} else if (smallView && file?.showStatusIconInSmallView == true) {
Box(Modifier.align(Alignment.Center)) {
loadingIndicator()
}
@@ -266,6 +276,9 @@ fun CIImageView(
}
}
private fun showDownloadButton(status: CIFileStatus?): Boolean =
status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted
@Composable
expect fun SimpleAndAnimatedImageView(
data: ByteArray,

View File

@@ -19,7 +19,9 @@ import chat.simplex.res.MR
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.chatViewScrollState
import dev.icerock.moko.resources.StringResource
import java.io.File
import java.net.URI
@@ -34,8 +36,10 @@ fun CIVideoView(
smallView: Boolean = false,
receiveFile: (Long) -> Unit
) {
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
Box(
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID)
.desktopModifyBlurredState(!smallView, blurred, showMenu),
contentAlignment = Alignment.TopEnd
) {
val preview = remember(image) { base64ToBitmap(image) }
@@ -68,15 +72,15 @@ fun CIVideoView(
if (decrypted != null && smallView) {
SmallVideoView(decrypted, file, preview, duration * 1000L, autoPlay, sizeMultiplier, openFullscreen = openFullscreen)
} else if (decrypted != null) {
VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen)
VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen)
} else if (smallView) {
SmallVideoViewEncrypted(uriDecrypted, file, preview, autoPlay, showMenu, sizeMultiplier, openFullscreen = openFullscreen)
} else {
VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen)
VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen)
}
} else {
Box {
VideoPreviewImageView(preview, onClick = {
VideoPreviewImageView(preview, blurred = blurred, onClick = {
if (file != null) {
when (file.fileStatus) {
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
@@ -109,14 +113,15 @@ fun CIVideoView(
if (file != null && !smallView) {
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
}
if (file?.fileStatus is CIFileStatus.RcvInvitation || file?.fileStatus is CIFileStatus.RcvAborted) {
if (showDownloadButton(file?.fileStatus) && !blurred.value && file != null) {
PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
}
}
}
if (!smallView) {
// Do not show download icon when the view is blurred
if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) {
fileStatusIcon(file, false)
} else if (file?.showStatusIconInSmallView == true) {
} else if (smallView && file?.showStatusIconInSmallView == true) {
Box(Modifier.align(Alignment.Center)) {
fileStatusIcon(file, true)
}
@@ -132,15 +137,16 @@ private fun VideoViewEncrypted(
defaultDuration: Long,
autoPlay: MutableState<Boolean>,
showMenu: MutableState<Boolean>,
blurred: MutableState<Boolean>,
openFullscreen: () -> Unit,
) {
var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) }
val onLongClick = { showMenu.value = true }
Box {
VideoPreviewImageView(defaultPreview, smallView = false, if (decryptionInProgress) {{}} else openFullscreen, onLongClick)
VideoPreviewImageView(defaultPreview, smallView = false, blurred = blurred, if (decryptionInProgress) {{}} else openFullscreen, onLongClick)
if (decryptionInProgress) {
VideoDecryptionProgress(1f, onLongClick = onLongClick)
} else {
} else if (!blurred.value) {
PlayButton(false, 1f, onLongClick = onLongClick) {
decryptionInProgress = true
withBGApi {
@@ -170,7 +176,7 @@ private fun SmallVideoViewEncrypted(
var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) }
val onLongClick = { showMenu.value = true }
Box {
VideoPreviewImageView(defaultPreview, smallView = true, if (decryptionInProgress) {{}} else openFullscreen, onLongClick)
VideoPreviewImageView(defaultPreview, smallView = true, blurred = remember { mutableStateOf(false) }, onClick = if (decryptionInProgress) {{}} else openFullscreen, onLongClick = onLongClick)
if (decryptionInProgress) {
VideoDecryptionProgress(sizeMultiplier, onLongClick = onLongClick)
} else if (!file.showStatusIconInSmallView) {
@@ -190,7 +196,15 @@ private fun SmallVideoViewEncrypted(
}
@Composable
private fun SmallVideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, autoPlay: MutableState<Boolean>, sizeMultiplier: Float, openFullscreen: () -> Unit) {
private fun SmallVideoView(
uri: URI,
file: CIFile,
defaultPreview: ImageBitmap,
defaultDuration: Long,
autoPlay: MutableState<Boolean>,
sizeMultiplier: Float,
openFullscreen: () -> Unit
) {
val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, defaultDuration, true) }
val preview by remember { player.preview }
// val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled }
@@ -205,7 +219,7 @@ private fun SmallVideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap,
onLongClick = {},
{}
)
VideoPreviewImageView(preview, smallView = true, openFullscreen, onLongClick = {})
VideoPreviewImageView(preview, smallView = true, blurred = remember { mutableStateOf(false) }, onClick = openFullscreen, onLongClick = {})
if (!file.showStatusIconInSmallView) {
PlayButton(brokenVideo, sizeMultiplier, onLongClick = {}, onClick = openFullscreen)
}
@@ -216,7 +230,16 @@ private fun SmallVideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap,
}
@Composable
private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, autoPlay: MutableState<Boolean>, showMenu: MutableState<Boolean>, openFullscreen: () -> Unit) {
private fun VideoView(
uri: URI,
file: CIFile,
defaultPreview: ImageBitmap,
defaultDuration: Long,
autoPlay: MutableState<Boolean>,
showMenu: MutableState<Boolean>,
blurred: MutableState<Boolean>,
openFullscreen: () -> Unit
) {
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 }
@@ -257,8 +280,8 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
stop
)
if (showPreview.value) {
VideoPreviewImageView(preview, smallView = false, openFullscreen, onLongClick)
if (!autoPlay.value) {
VideoPreviewImageView(preview, smallView = false, blurred = blurred, openFullscreen, onLongClick)
if (!autoPlay.value && !blurred.value) {
PlayButton(brokenVideo, onLongClick = onLongClick, onClick = play)
}
}
@@ -365,7 +388,13 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
}
@Composable
fun VideoPreviewImageView(preview: ImageBitmap, smallView: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) {
fun VideoPreviewImageView(
preview: ImageBitmap,
smallView: Boolean,
blurred: MutableState<Boolean>,
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(
@@ -377,7 +406,8 @@ fun VideoPreviewImageView(preview: ImageBitmap, smallView: Boolean, onClick: ()
onLongClick = onLongClick,
onClick = onClick
)
.onRightClick(onLongClick),
.onRightClick(onLongClick)
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = onLongClick),
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
)
}
@@ -522,6 +552,9 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
}
}
private fun showDownloadButton(status: CIFileStatus?): Boolean =
status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted
private fun fileSizeValid(file: CIFile?): Boolean {
if (file != null) {
return file.fileSize <= getMaxFileSize(file.fileProtocol)

View File

@@ -267,7 +267,7 @@ fun FramedItemView(
ciFileView(ci, mc.text)
}
is MsgContent.MCLink -> {
ChatItemLinkView(mc.preview)
ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true })
Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
}

View File

@@ -1,12 +1,11 @@
package chat.simplex.common.views.helpers
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
@@ -15,9 +14,11 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.LinkPreview
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.chatViewScrollState
import chat.simplex.res.MR
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -121,12 +122,16 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel
}
@Composable
fun ChatItemLinkView(linkPreview: LinkPreview) {
fun ChatItemLinkView(linkPreview: LinkPreview, showMenu: State<Boolean>, onLongClick: () -> Unit) {
Column(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) {
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
Image(
base64ToBitmap(linkPreview.image),
stringResource(MR.strings.image_descr_link_preview),
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.desktopModifyBlurredState(true, blurred, showMenu)
.privacyBlur(true, blurred, chatViewScrollState.collectAsState(), onLongClick = onLongClick),
contentScale = ContentScale.FillWidth,
)
Column(Modifier.padding(top = 6.dp).padding(horizontal = 12.dp)) {
@@ -179,7 +184,7 @@ private fun normalizeImageUri(u: URL, imageUri: String) = when {
@Composable
fun PreviewChatItemLinkView() {
SimpleXTheme {
ChatItemLinkView(LinkPreview.sampleData)
ChatItemLinkView(LinkPreview.sampleData, remember { mutableStateOf(false) }) {}
}
}

View File

@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.res.MR
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.ProfileNameField
import chat.simplex.common.views.helpers.*
@@ -32,6 +33,8 @@ import chat.simplex.common.views.localauth.SetAppPasscodeView
import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import kotlin.math.min
import kotlin.math.roundToInt
enum class LAMode {
SYSTEM,
@@ -95,6 +98,9 @@ fun PrivacySettingsView(
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
})
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) {
appPrefs.privacyMediaBlurRadius.set(it)
}
SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays)
}
SectionCustomFooter {
@@ -217,6 +223,30 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onS
)
}
@Composable
private fun BlurRadiusOptions(state: State<Int>, onSelected: (Int) -> Unit) {
val choices = listOf(0, 12, 24, 48)
val pickerValues = choices + if (choices.contains(state.value)) emptyList() else listOf(state.value)
val values = remember {
pickerValues.map {
when (it) {
0 -> it to generalGetString(MR.strings.privacy_media_blur_radius_off)
12 -> it to generalGetString(MR.strings.privacy_media_blur_radius_soft)
24 -> it to generalGetString(MR.strings.privacy_media_blur_radius_medium)
48 -> it to generalGetString(MR.strings.privacy_media_blur_radius_strong)
else -> it to "$it"
}
}
}
ExposedDropDownSettingRow(
generalGetString(MR.strings.privacy_media_blur_radius),
values,
state,
icon = painterResource(MR.images.ic_blur_on),
onSelected = onSelected
)
}
@Composable
expect fun PrivacyDeviceSection(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),

View File

@@ -1102,6 +1102,11 @@
<string name="receipts_groups_disable_keep_overrides">Disable (keep group overrides)</string>
<string name="receipts_groups_enable_for_all">Enable for all groups</string>
<string name="receipts_groups_disable_for_all">Disable for all groups</string>
<string name="privacy_media_blur_radius">Blur media</string>
<string name="privacy_media_blur_radius_off">Off</string>
<string name="privacy_media_blur_radius_soft">Soft</string>
<string name="privacy_media_blur_radius_medium">Medium</string>
<string name="privacy_media_blur_radius_strong">Strong</string>
<!-- Settings sections -->
<string name="settings_section_title_you">YOU</string>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M124-382q-9.5 0-15.25-6T103-403q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm0-155q-9.5 0-15.25-6T103-558q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm117.96 332.5q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77 10.59-10.89 26.5-10.89t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-161q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77 10.59-10.89 26.5-10.89t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-154.5q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77Q225.68-595 241.59-595t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77Q257.91-520 241.96-520Zm0-161q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77Q225.68-756 241.59-756t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77Q257.91-681 241.96-681Zm160.91 331q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44Q456-425.12 456-403.06q0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm0-154.5q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44Q456-579.62 456-557.56q0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm.09 300q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77 10.59-10.89 26.5-10.89t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-476.5q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77Q386.68-756 402.59-756t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77Q418.91-681 402.96-681Zm.04 578q-9.5 0-15.25-6T382-124q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm0-712.5q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6ZM557.37-350q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44 15.75 15.44 15.75 37.5 0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm0-154.5q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44 15.75 15.44 15.75 37.5 0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm.09 300q-15.96 0-26.71-10.74Q520-225.97 520-241.84q0-15.87 10.59-26.77 10.59-10.89 26.5-10.89T584-268.76q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-476.5q-15.96 0-26.71-10.74Q520-702.47 520-718.34q0-15.87 10.59-26.77Q541.18-756 557.09-756T584-745.26q11 10.73 11 26.6 0 15.87-10.79 26.77Q573.41-681 557.46-681ZM564-103q-9.5 0-15.25-6T543-124q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm-6-712.5q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm160.46 611q-15.96 0-26.71-10.74Q681-225.97 681-241.84q0-15.87 10.59-26.77 10.59-10.89 26.5-10.89T745-268.76q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-161q-15.96 0-26.71-10.74Q681-386.97 681-402.84q0-15.87 10.59-26.77 10.59-10.89 26.5-10.89T745-429.76q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-154.5q-15.96 0-26.71-10.74Q681-541.47 681-557.34q0-15.87 10.59-26.77Q702.18-595 718.09-595T745-584.26q11 10.73 11 26.6 0 15.87-10.79 26.77Q734.41-520 718.46-520Zm0-161q-15.96 0-26.71-10.74Q681-702.47 681-718.34q0-15.87 10.59-26.77Q702.18-756 718.09-756T745-745.26q11 10.73 11 26.6 0 15.87-10.79 26.77Q734.41-681 718.46-681ZM836.5-382q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm0-155q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Z"/></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -4,8 +4,7 @@ import androidx.compose.foundation.contextMenuOpenDetector
import androidx.compose.runtime.Composable
import androidx.compose.ui.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.input.pointer.*
import java.io.File
import java.net.URI
@@ -40,3 +39,8 @@ onExternalDrag(enabled) {
actual fun Modifier.onRightClick(action: () -> Unit): Modifier = contextMenuOpenDetector { action() }
actual fun Modifier.desktopPointerHoverIconHand(): Modifier = Modifier.pointerHoverIcon(PointerIcon.Hand)
actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier =
this then Modifier
.onPointerEvent(PointerEventType.Enter) { action(true) }
.onPointerEvent(PointerEventType.Exit) { action(false) }