Merge remote-tracking branch 'origin/main' into claude/improve-desktop-design-iOokB

This commit is contained in:
Claude
2026-04-24 18:41:58 +00:00
23 changed files with 473 additions and 80 deletions
@@ -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)
@@ -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,
)
}
}
}
}
}
@@ -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) }
@@ -224,6 +224,10 @@ private fun AccountPicture(
modifier = AccountPictureModifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
autoPlayGif =
accountViewModel.settings.autoPlayVideosFlow
.collectAsStateWithLifecycle()
.value,
)
}
@@ -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) {
@@ -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
}
@@ -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,
)
}
}
@@ -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,
)
}
@@ -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,
)
}
}
@@ -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,
)
}
@@ -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,
)
}
@@ -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,
@@ -415,6 +415,10 @@ private fun RenderBadgeImage(
modifier = modifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
autoPlayGif =
accountViewModel.settings.autoPlayVideosFlow
.collectAsStateWithLifecycle()
.value,
)
}
}
@@ -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(
@@ -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>
+1
View File
@@ -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>