refactor(media): simplify GIF rendering and wire autoplay setting

Replaces the video-style play/pause controls on GIFs with normal image
rendering. When Auto-play Videos is off, GIFs pause on the first frame
(feed GIFs additionally show a small "gif" label in the bottom-left);
tapping still opens the fullscreen dialog.

Profile-picture GIFs now respect the same setting. The key fix is
bypassing ProfilePictureFetcher (which serves a static JPEG thumbnail
and prevents animation); GIF avatars load via the raw URL so Coil's
animated decoder runs. Autoplay is read reactively from a new
autoPlayVideosFlow StateFlow so toggling the setting immediately
starts/stops animation in the drawer, top bar, and every other avatar.
This commit is contained in:
greenart7c3
2026-04-24 08:26:00 -03:00
parent e170b65753
commit bd222e3e9b
20 changed files with 219 additions and 76 deletions
@@ -21,44 +21,40 @@
package com.vitorpamplona.amethyst.ui.components
import android.graphics.drawable.Animatable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
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.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.OpenInFull
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.Size75dp
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
import kotlinx.coroutines.delay
@Composable
fun GifVideoView(
@@ -73,24 +69,17 @@ fun GifVideoView(
thumbhash: String? = null,
) {
val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri)
val automaticallyStartPlayback = remember { mutableStateOf(accountViewModel.settings.autoPlayVideos()) }
val controllerVisible = remember { mutableStateOf(false) }
LaunchedEffect(controllerVisible.value, automaticallyStartPlayback.value) {
if (controllerVisible.value && automaticallyStartPlayback.value) {
delay(3000)
controllerVisible.value = false
}
}
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 =
(if (ratio != null) borderModifier.aspectRatio(ratio) else borderModifier)
.fillMaxWidth()
.clickable { controllerVisible.value = !controllerVisible.value },
modifier = containerModifier,
contentAlignment = Alignment.Center,
) {
SubcomposeAsyncImage(
@@ -104,13 +93,9 @@ fun GifVideoView(
val successState = state as? AsyncImagePainter.State.Success
val drawable = successState?.result?.image?.asDrawable(context.resources)
LaunchedEffect(automaticallyStartPlayback.value, drawable) {
LaunchedEffect(autoPlay, drawable) {
if (drawable is Animatable) {
if (automaticallyStartPlayback.value) {
drawable.start()
} else {
drawable.stop()
}
if (autoPlay) drawable.start() else drawable.stop()
}
}
@@ -127,6 +112,11 @@ fun GifVideoView(
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 -> {
@@ -137,49 +127,21 @@ fun GifVideoView(
}
}
// Zoom button
AnimatedVisibility(
visible = controllerVisible.value,
modifier = Modifier.align(Alignment.TopEnd),
enter = fadeIn(),
exit = fadeOut(),
) {
IconButton(
onClick = { onDialog?.invoke() },
) {
Icon(
imageVector = Icons.Outlined.OpenInFull,
contentDescription = null,
tint = Color.White,
)
}
}
// Play/Pause button
AnimatedVisibility(
visible = controllerVisible.value || !automaticallyStartPlayback.value,
enter = fadeIn(),
exit = fadeOut(),
) {
Box(
if (!autoPlay) {
Text(
text = stringResource(R.string.gif),
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = Font10SP,
lineHeight = Font10SP,
modifier =
Modifier
.fillMaxSize()
.background(if (!automaticallyStartPlayback.value) Color.Black.copy(alpha = 0.3f) else Color.Transparent),
contentAlignment = Alignment.Center,
) {
IconButton(
modifier = Modifier.size(Size75dp),
onClick = { automaticallyStartPlayback.value = !automaticallyStartPlayback.value },
) {
Icon(
imageVector = if (automaticallyStartPlayback.value) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(Size75dp),
)
}
}
.align(Alignment.BottomStart)
.padding(6.dp)
.clip(SmallBorder)
.background(Color.Black.copy(alpha = 0.6f))
.padding(horizontal = 4.dp, vertical = 1.dp),
)
}
}
}
@@ -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,
)
}
}
}
}
}
@@ -225,6 +225,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,
)
}
@@ -56,6 +56,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
@@ -1818,6 +1819,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,
)
}
@@ -328,6 +328,10 @@ fun ShortCommunityHeader(
modifier = HeaderPictureModifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
autoPlayGif =
accountViewModel.settings.autoPlayVideosFlow
.collectAsStateWithLifecycle()
.value,
)
}
@@ -378,6 +382,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,
@@ -416,6 +416,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(
@@ -568,6 +568,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)) },
)
@@ -597,6 +601,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)) },
)
@@ -624,6 +632,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)) },
)
+1
View File
@@ -2506,4 +2506,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>