Merge remote-tracking branch 'origin/main' into claude/improve-desktop-design-iOokB
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.graphics.drawable.Animatable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.asDrawable
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.SubcomposeAsyncImageContent
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font10SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||
|
||||
@Composable
|
||||
fun GifVideoView(
|
||||
videoUri: String,
|
||||
contentDescription: String?,
|
||||
dimensions: DimensionTag?,
|
||||
blurhash: String?,
|
||||
roundedCorner: Boolean,
|
||||
contentScale: ContentScale,
|
||||
onDialog: (() -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
thumbhash: String? = null,
|
||||
) {
|
||||
val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri)
|
||||
val autoPlay = accountViewModel.settings.autoPlayVideos()
|
||||
val borderModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier
|
||||
val context = LocalContext.current
|
||||
|
||||
val containerModifier =
|
||||
(if (ratio != null) borderModifier.aspectRatio(ratio) else borderModifier)
|
||||
.fillMaxWidth()
|
||||
.let { if (onDialog != null) it.clickable { onDialog() } else it }
|
||||
|
||||
Box(
|
||||
modifier = containerModifier,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
model = videoUri,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = contentScale,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val state by painter.state.collectAsState()
|
||||
|
||||
val successState = state as? AsyncImagePainter.State.Success
|
||||
val drawable = successState?.result?.image?.asDrawable(context.resources)
|
||||
|
||||
LaunchedEffect(autoPlay, drawable) {
|
||||
if (drawable is Animatable) {
|
||||
if (autoPlay) drawable.start() else drawable.stop()
|
||||
}
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is AsyncImagePainter.State.Loading -> {
|
||||
DisplayBlurHash(
|
||||
blurhash,
|
||||
contentDescription,
|
||||
contentScale,
|
||||
Modifier.fillMaxSize(),
|
||||
thumbhash = thumbhash,
|
||||
)
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
SubcomposeAsyncImageContent(Modifier.fillMaxSize())
|
||||
|
||||
SideEffect {
|
||||
val image = (state as AsyncImagePainter.State.Success).result.image
|
||||
MediaAspectRatioCache.add(videoUri, image.width, image.height)
|
||||
}
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
ClickableUrl(urlText = videoUri, url = videoUri)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
if (!autoPlay) {
|
||||
Text(
|
||||
text = stringResource(R.string.gif),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = Font10SP,
|
||||
lineHeight = Font10SP,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(6.dp)
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
.padding(horizontal = 4.dp, vertical = 1.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,86 +56,105 @@ fun MyAsyncImage(
|
||||
onLoadingBackground: (@Composable () -> Unit)?,
|
||||
onError: (@Composable () -> Unit)?,
|
||||
) {
|
||||
val ratio = MediaAspectRatioCache.get(imageUrl)
|
||||
val showImage = remember { mutableStateOf(accountViewModel.settings.showImages()) }
|
||||
if (isGifUrl(imageUrl)) {
|
||||
GifVideoView(
|
||||
videoUri = imageUrl,
|
||||
contentDescription = contentDescription,
|
||||
dimensions = null,
|
||||
blurhash = null,
|
||||
roundedCorner = contentScale == ContentScale.Crop,
|
||||
contentScale = contentScale,
|
||||
onDialog = null,
|
||||
accountViewModel = accountViewModel,
|
||||
thumbhash = null,
|
||||
)
|
||||
} else {
|
||||
val ratio = MediaAspectRatioCache.get(imageUrl)
|
||||
val showImage = remember { mutableStateOf(accountViewModel.settings.showImages()) }
|
||||
|
||||
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
SubcomposeAsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = contentScale,
|
||||
modifier = mainImageModifier,
|
||||
) {
|
||||
val state by painter.state.collectAsState()
|
||||
when (state) {
|
||||
is AsyncImagePainter.State.Loading -> {
|
||||
if (ratio != null) {
|
||||
Box(loadedImageModifier.aspectRatio(ratio), contentAlignment = Alignment.Center) {
|
||||
LoadingAnimation(Size40dp, Size6dp)
|
||||
}
|
||||
} else {
|
||||
WaitAndDisplay {
|
||||
if (onLoadingBackground != null) {
|
||||
Box(loadedImageModifier, contentAlignment = Alignment.Center) {
|
||||
onLoadingBackground()
|
||||
LoadingAnimation(Size40dp, Size6dp)
|
||||
}
|
||||
} else {
|
||||
DisplayUrlWithLoadingSymbol(imageUrl, accountViewModel.toastManager::toast)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
if (onError != null) {
|
||||
CrossfadeIfEnabled(targetState = showImage.value, contentAlignment = Alignment.Center, accountViewModel = accountViewModel) {
|
||||
if (it) {
|
||||
SubcomposeAsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = contentScale,
|
||||
modifier = mainImageModifier,
|
||||
) {
|
||||
val state by painter.state.collectAsState()
|
||||
when (state) {
|
||||
is AsyncImagePainter.State.Loading -> {
|
||||
if (ratio != null) {
|
||||
Box(loadedImageModifier.aspectRatio(ratio), contentAlignment = Alignment.Center) {
|
||||
onError()
|
||||
LoadingAnimation(Size40dp, Size6dp)
|
||||
}
|
||||
} else {
|
||||
Box(loadedImageModifier, contentAlignment = Alignment.Center) {
|
||||
onError()
|
||||
WaitAndDisplay {
|
||||
if (onLoadingBackground != null) {
|
||||
Box(loadedImageModifier, contentAlignment = Alignment.Center) {
|
||||
onLoadingBackground()
|
||||
LoadingAnimation(Size40dp, Size6dp)
|
||||
}
|
||||
} else {
|
||||
DisplayUrlWithLoadingSymbol(imageUrl, accountViewModel.toastManager::toast)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ClickableUrl(urlText = imageUrl, url = imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
SubcomposeAsyncImageContent(loadedImageModifier)
|
||||
|
||||
SideEffect {
|
||||
val drawable = (state as AsyncImagePainter.State.Success).result.image
|
||||
MediaAspectRatioCache.add(imageUrl, drawable.width, drawable.height)
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
if (onError != null) {
|
||||
if (ratio != null) {
|
||||
Box(loadedImageModifier.aspectRatio(ratio), contentAlignment = Alignment.Center) {
|
||||
onError()
|
||||
}
|
||||
} else {
|
||||
Box(loadedImageModifier, contentAlignment = Alignment.Center) {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ClickableUrl(urlText = imageUrl, url = imageUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (ratio != null) {
|
||||
Box(loadedImageModifier.aspectRatio(ratio), contentAlignment = Alignment.Center) {
|
||||
IconButton(
|
||||
modifier = Modifier.size(Size75dp),
|
||||
onClick = { showImage.value = true },
|
||||
) {
|
||||
DownloadForOfflineIcon(Size75dp, MaterialTheme.colorScheme.onBackground)
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
SubcomposeAsyncImageContent(loadedImageModifier)
|
||||
|
||||
SideEffect {
|
||||
val drawable = (state as AsyncImagePainter.State.Success).result.image
|
||||
MediaAspectRatioCache.add(imageUrl, drawable.width, drawable.height)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(loadedImageModifier.aspectRatio(16 / 9.0f), contentAlignment = Alignment.Center) {
|
||||
IconButton(
|
||||
modifier = Modifier.size(Size75dp),
|
||||
onClick = { showImage.value = true },
|
||||
) {
|
||||
DownloadForOfflineIcon(Size75dp, MaterialTheme.colorScheme.onBackground)
|
||||
if (ratio != null) {
|
||||
Box(loadedImageModifier.aspectRatio(ratio), contentAlignment = Alignment.Center) {
|
||||
IconButton(
|
||||
modifier = Modifier.size(Size75dp),
|
||||
onClick = { showImage.value = true },
|
||||
) {
|
||||
DownloadForOfflineIcon(Size75dp, MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(loadedImageModifier.aspectRatio(16 / 9.0f), contentAlignment = Alignment.Center) {
|
||||
IconButton(
|
||||
modifier = Modifier.size(Size75dp),
|
||||
onClick = { showImage.value = true },
|
||||
) {
|
||||
DownloadForOfflineIcon(Size75dp, MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isGifUrl(url: String): Boolean =
|
||||
url.endsWith(".gif", ignoreCase = true) ||
|
||||
url.contains(".gif?", ignoreCase = true) ||
|
||||
url.contains(".gif#", ignoreCase = true)
|
||||
|
||||
+77
-1
@@ -20,9 +20,15 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.graphics.drawable.Animatable
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
@@ -31,7 +37,12 @@ import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil3.asDrawable
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.SubcomposeAsyncImageContent
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.rememberMaterialSymbolPainter
|
||||
import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash
|
||||
@@ -75,8 +86,18 @@ fun RobohashFallbackAsyncImage(
|
||||
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
|
||||
loadProfilePicture: Boolean,
|
||||
loadRobohash: Boolean,
|
||||
autoPlayGif: Boolean = true,
|
||||
) {
|
||||
if (model != null && loadProfilePicture) {
|
||||
if (model != null && loadProfilePicture && isGifUrl(model)) {
|
||||
GifProfilePicture(
|
||||
userHex = robot,
|
||||
userPicture = model,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
loadRobohash = loadRobohash,
|
||||
autoPlay = autoPlayGif,
|
||||
)
|
||||
} else if (model != null && loadProfilePicture) {
|
||||
val painter =
|
||||
if (loadRobohash) {
|
||||
rememberVectorPainter(
|
||||
@@ -124,3 +145,58 @@ fun RobohashFallbackAsyncImage(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GifProfilePicture(
|
||||
userHex: String,
|
||||
userPicture: String,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier,
|
||||
loadRobohash: Boolean,
|
||||
autoPlay: Boolean,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
val fallbackPainter =
|
||||
if (loadRobohash) {
|
||||
rememberVectorPainter(image = CachedRobohash.get(userHex, MaterialTheme.colorScheme.isLight))
|
||||
} else {
|
||||
forwardingPainter(
|
||||
painter = rememberMaterialSymbolPainter(MaterialSymbols.Face),
|
||||
colorFilter = MaterialTheme.colorScheme.onBackgroundColorFilter,
|
||||
)
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
SubcomposeAsyncImage(
|
||||
model = userPicture,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val state by painter.state.collectAsState()
|
||||
val successState = state as? AsyncImagePainter.State.Success
|
||||
val drawable = successState?.result?.image?.asDrawable(resources)
|
||||
|
||||
LaunchedEffect(drawable, autoPlay) {
|
||||
if (drawable is Animatable) {
|
||||
if (autoPlay) drawable.start() else drawable.stop()
|
||||
}
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
SubcomposeAsyncImageContent()
|
||||
}
|
||||
|
||||
else -> {
|
||||
Image(
|
||||
painter = fallbackPainter,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+55
-16
@@ -159,14 +159,28 @@ fun ZoomableContentView(
|
||||
modifier = mediaSizingModifier(ratio, contentScale),
|
||||
backdrop = (content.thumbhash ?: content.blurhash)?.let { { BlurhashBackdrop(content.blurhash, content.description, content.thumbhash) } },
|
||||
) {
|
||||
TwoSecondController(content) { controllerVisible ->
|
||||
val mainImageModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.then(boundsTrackingModifier)
|
||||
.clickable { dialogOpen = true }
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
UrlImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
if (content.isGif()) {
|
||||
GifVideoView(
|
||||
videoUri = content.url,
|
||||
contentDescription = content.description,
|
||||
dimensions = content.dim,
|
||||
blurhash = content.blurhash,
|
||||
roundedCorner = roundedCorner,
|
||||
contentScale = contentScale,
|
||||
onDialog = { dialogOpen = true },
|
||||
accountViewModel = accountViewModel,
|
||||
thumbhash = content.thumbhash,
|
||||
)
|
||||
} else {
|
||||
TwoSecondController(content) { controllerVisible ->
|
||||
val mainImageModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.then(boundsTrackingModifier)
|
||||
.clickable { dialogOpen = true }
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
UrlImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,15 +220,31 @@ fun ZoomableContentView(
|
||||
}
|
||||
|
||||
is MediaLocalImage -> {
|
||||
TwoSecondController(content) { controllerVisible ->
|
||||
val mainImageModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.then(boundsTrackingModifier)
|
||||
.clickable { dialogOpen = true }
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
if (content.isGif()) {
|
||||
content.localFile?.let {
|
||||
GifVideoView(
|
||||
videoUri = it.toUri().toString(),
|
||||
contentDescription = content.description,
|
||||
dimensions = content.dim,
|
||||
blurhash = content.blurhash,
|
||||
roundedCorner = roundedCorner,
|
||||
contentScale = contentScale,
|
||||
onDialog = { dialogOpen = true },
|
||||
accountViewModel = accountViewModel,
|
||||
thumbhash = content.thumbhash,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TwoSecondController(content) { controllerVisible ->
|
||||
val mainImageModifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.then(boundsTrackingModifier)
|
||||
.clickable { dialogOpen = true }
|
||||
val loadedImageModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier.fillMaxWidth()
|
||||
|
||||
LocalImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
LocalImageView(content, contentScale, mainImageModifier, loadedImageModifier, controllerVisible, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,6 +677,15 @@ fun ShowHash(content: MediaUrlContent) {
|
||||
verifiedHash?.let { HashVerificationSymbol(it) }
|
||||
}
|
||||
|
||||
fun BaseMediaContent.isGif(): Boolean =
|
||||
if (this is MediaUrlContent) {
|
||||
mimeType == "image/gif" || url.endsWith(".gif", ignoreCase = true) || url.contains(".gif?", ignoreCase = true) || url.contains(".gif#", ignoreCase = true)
|
||||
} else if (this is MediaPreloadedContent) {
|
||||
mimeType == "image/gif" || localFile?.name?.endsWith(".gif", ignoreCase = true) == true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WaitAndDisplay(content: @Composable (AnimatedVisibilityScope.() -> Unit)) {
|
||||
val visible = remember { mutableStateOf(false) }
|
||||
|
||||
+4
@@ -224,6 +224,10 @@ private fun AccountPicture(
|
||||
modifier = AccountPictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -259,6 +259,10 @@ fun ProfileContentTemplate(
|
||||
.clickable(onClick = onClick),
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
|
||||
if (bestDisplayName != null) {
|
||||
|
||||
+5
@@ -31,6 +31,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
@@ -87,6 +88,10 @@ private fun LoggedInUserPictureDrawer(
|
||||
contentScale = ContentScale.Crop,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
@@ -623,6 +624,10 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
|
||||
contentScale = ContentScale.Crop,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
@@ -1817,6 +1818,10 @@ private fun ChannelNotePicture(
|
||||
modifier = MaterialTheme.colorScheme.channelNotePictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -637,6 +637,10 @@ fun InnerUserPicture(
|
||||
contentScale = ContentScale.Crop,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -327,6 +327,10 @@ fun ShortCommunityHeader(
|
||||
modifier = HeaderPictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -377,6 +381,10 @@ fun ShortCommunityHeaderNoActions(
|
||||
modifier = HeaderPictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@Stable
|
||||
@@ -140,5 +141,14 @@ class UiSettingsState(
|
||||
|
||||
fun autoPlayVideos() = uiSettingsFlow.automaticallyPlayVideos.value == BooleanType.ALWAYS
|
||||
|
||||
val autoPlayVideosFlow: StateFlow<Boolean> =
|
||||
uiSettingsFlow.automaticallyPlayVideos
|
||||
.map { it == BooleanType.ALWAYS }
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
uiSettingsFlow.automaticallyPlayVideos.value == BooleanType.ALWAYS,
|
||||
)
|
||||
|
||||
fun showImages() = showImages.value
|
||||
}
|
||||
|
||||
+5
@@ -47,6 +47,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.defaults.Constants
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
@@ -162,6 +163,10 @@ fun RenderChannelData(
|
||||
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||
@@ -103,6 +104,10 @@ private fun DrawRelayIcon(
|
||||
modifier = HeaderPictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -40,6 +40,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
@@ -89,6 +90,10 @@ fun LongPublicChatChannelHeader(
|
||||
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
|
||||
@@ -72,6 +73,10 @@ fun ShortPublicChatChannelHeader(
|
||||
modifier = HeaderPictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -33,6 +33,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
@@ -73,6 +74,10 @@ fun ChannelFileUploadDialog(
|
||||
modifier = HeaderPictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+14
@@ -209,6 +209,10 @@ private fun ChannelRoomCompose(
|
||||
hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
onClick = { nav.nav(routeFor(channel)) },
|
||||
)
|
||||
}
|
||||
@@ -241,6 +245,10 @@ private fun ChannelRoomCompose(
|
||||
hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
onClick = { nav.nav(routeFor(channel)) },
|
||||
)
|
||||
}
|
||||
@@ -276,6 +284,10 @@ private fun MarmotGroupRoomCompose(
|
||||
hasNewMessages = unread > 0,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
onClick = { nav.nav(Route.MarmotGroupChat(chatroom.nostrGroupId)) },
|
||||
)
|
||||
}
|
||||
@@ -399,6 +411,7 @@ fun ChannelName(
|
||||
hasNewMessages: Boolean,
|
||||
loadProfilePicture: Boolean,
|
||||
loadRobohash: Boolean,
|
||||
autoPlayGif: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ChannelName(
|
||||
@@ -410,6 +423,7 @@ fun ChannelName(
|
||||
modifier = AccountPictureModifier,
|
||||
loadProfilePicture = loadProfilePicture,
|
||||
loadRobohash = loadRobohash,
|
||||
autoPlayGif = autoPlayGif,
|
||||
)
|
||||
},
|
||||
channelTitle,
|
||||
|
||||
+4
@@ -415,6 +415,10 @@ private fun RenderBadgeImage(
|
||||
modifier = modifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -232,6 +232,10 @@ fun RenderName(
|
||||
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
|
||||
+12
@@ -567,6 +567,10 @@ private fun DisplaySearchResults(
|
||||
hasNewMessages = false,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
onClick = { nav.nav(routeFor(item)) },
|
||||
)
|
||||
|
||||
@@ -596,6 +600,10 @@ private fun DisplaySearchResults(
|
||||
hasNewMessages = false,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
onClick = { nav.nav(routeFor(item)) },
|
||||
)
|
||||
|
||||
@@ -623,6 +631,10 @@ private fun DisplaySearchResults(
|
||||
hasNewMessages = false,
|
||||
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
|
||||
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
|
||||
autoPlayGif =
|
||||
accountViewModel.settings.autoPlayVideosFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
.value,
|
||||
onClick = { nav.nav(routeFor(item)) },
|
||||
)
|
||||
|
||||
|
||||
@@ -771,6 +771,13 @@
|
||||
<string name="app_notification_chess_challenge_accepted">%1$s zaakceptował Twoją ofertę</string>
|
||||
<string name="app_notification_chess_your_turn">%1$s wykonał ruch — teraz Twoja kolej</string>
|
||||
<string name="app_notification_chess_summary">Aktualizacje szachów</string>
|
||||
<string name="app_notification_replies_channel_name">Odpowiedzi</string>
|
||||
<string name="app_notification_replies_channel_description">Powiadamia Cię, gdy ktoś zareaguje na twój post</string>
|
||||
<string name="app_notification_replies_channel_message">%1$s odpowiedział</string>
|
||||
<string name="app_notification_replies_channel_message_for">na: %1$s</string>
|
||||
<string name="app_notification_replies_summary">Nowe odpowiedzi</string>
|
||||
<string name="app_notification_mentions_channel_name">Wzmianki</string>
|
||||
<string name="app_notification_mentions_channel_description">Powiadamia Cię, gdy ktoś wspomni o Tobie lub zacytuje Cię w poście</string>
|
||||
<!-- Call notifications and UI -->
|
||||
<string name="app_notification_calls_channel_name">Połączenia przychodzące</string>
|
||||
<string name="app_notification_calls_channel_description">Powiadomienia o przychodzących połączeniach głosowych i wideo</string>
|
||||
|
||||
@@ -2517,4 +2517,5 @@
|
||||
<string name="emoji_private_badge">Private emoji</string>
|
||||
<string name="emoji_public_explainer">Public emojis are visible to everyone and appear in your reaction menu and \":\" autocomplete picker when this pack is in your emoji list.</string>
|
||||
<string name="emoji_private_explainer">Private emojis are stored encrypted on relays and visible only to you. They appear in your reaction menu and \":\" autocomplete just like public ones.</string>
|
||||
<string name="gif">Gif</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user