diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/AppScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/AppScreen.kt index 5f05f9d41..0b6d37eb3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/AppScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/AppScreen.kt @@ -45,6 +45,10 @@ fun prepareSharedViewModel(act: MainActivity): SharedPreferencesViewModel { sharedPreferencesViewModel.updateDisplaySettings(windowSizeClass, displayFeatures) } + LaunchedEffect(key1 = displayFeatures, key2 = windowSizeClass) { + sharedPreferencesViewModel.updateDisplaySettings(windowSizeClass, displayFeatures) + } + LaunchedEffect(act.isOnMobileDataState) { sharedPreferencesViewModel.updateConnectivityStatusState(act.isOnMobileDataState) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 4f5cf65e3..ff3ae9966 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -305,6 +305,7 @@ fun EditPostView( VideoView( myUrlPreview, roundedCorner = true, + isFiniteHeight = false, accountViewModel = accountViewModel, ) } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index f68a9fd3d..63f379ec3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -252,6 +252,7 @@ fun ImageVideoPost( VideoView( videoUri = it.toString(), roundedCorner = false, + isFiniteHeight = false, accountViewModel = accountViewModel, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 2a84f6216..4734f49d4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -426,6 +426,7 @@ fun NewPostView( VideoView( myUrlPreview, roundedCorner = true, + isFiniteHeight = false, accountViewModel = accountViewModel, ) } else { @@ -1736,7 +1737,7 @@ fun ImageVideoDescription( ) } } else { - VideoView(uri.toString(), roundedCorner = true, accountViewModel = accountViewModel) + VideoView(uri.toString(), roundedCorner = true, isFiniteHeight = false, accountViewModel = accountViewModel) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt index 702a01d09..2d91cab5c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt @@ -84,6 +84,7 @@ fun RenderLoaded( ZoomableContentView( content = MediaUrlImage(url, uri = callbackUri), roundedCorner = true, + isFiniteHeight = false, accountViewModel = accountViewModel, ) } @@ -92,6 +93,7 @@ fun RenderLoaded( ZoomableContentView( content = MediaUrlVideo(url, uri = callbackUri), roundedCorner = true, + isFiniteHeight = false, accountViewModel = accountViewModel, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 21d32f68e..7bcb04992 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -446,7 +446,7 @@ private fun ZoomableContentView( ) { state.imagesForPager[word]?.let { Box(modifier = HalfVertPadding) { - ZoomableContentView(it, state.imageList, roundedCorner = true, accountViewModel) + ZoomableContentView(it, state.imageList, roundedCorner = true, isFiniteHeight = false, accountViewModel) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt index 9f896cc51..621e24ab1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt @@ -36,7 +36,6 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize @@ -53,7 +52,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -72,13 +70,10 @@ import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.isFinite -import androidx.compose.ui.unit.isSpecified import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -100,7 +95,6 @@ import com.vitorpamplona.amethyst.ui.note.MuteIcon import com.vitorpamplona.amethyst.ui.note.MutedIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize -import com.vitorpamplona.amethyst.ui.theme.Size0dp import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.ui.theme.Size75dp @@ -127,6 +121,7 @@ fun LoadThumbAndThenVideoView( thumbUri: String, authorName: String? = null, roundedCorner: Boolean, + isFiniteHeight: Boolean, nostrUriCallback: String? = null, accountViewModel: AccountViewModel, onDialog: ((Boolean) -> Unit)? = null, @@ -156,6 +151,7 @@ fun LoadThumbAndThenVideoView( title = title, thumb = VideoThumb(loadingFinished.second), roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, artworkUri = thumbUri, authorName = authorName, nostrUriCallback = nostrUriCallback, @@ -168,6 +164,7 @@ fun LoadThumbAndThenVideoView( title = title, thumb = null, roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, artworkUri = thumbUri, authorName = authorName, nostrUriCallback = nostrUriCallback, @@ -184,6 +181,7 @@ fun VideoView( title: String? = null, thumb: VideoThumb? = null, roundedCorner: Boolean, + isFiniteHeight: Boolean, topPaddingForControllers: Dp = Dp.Unspecified, waveform: ImmutableList? = null, artworkUri: String? = null, @@ -208,7 +206,7 @@ fun VideoView( if (blurhash == null) { val ratio = aspectRatio(dimensions) val modifier = - if (ratio != null && roundedCorner && automaticallyStartPlayback.value) { + if (ratio != null && automaticallyStartPlayback.value) { Modifier.aspectRatio(ratio) } else { Modifier @@ -224,6 +222,7 @@ fun VideoView( title = title, thumb = thumb, roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, topPaddingForControllers = topPaddingForControllers, waveform = waveform, artworkUri = artworkUri, @@ -239,21 +238,31 @@ fun VideoView( } } else { val ratio = aspectRatio(dimensions) + val modifier = - if (ratio != null && roundedCorner) { + if (ratio != null) { Modifier.aspectRatio(ratio) } else { Modifier } Box(modifier, contentAlignment = Alignment.Center) { + val image = + if (roundedCorner) { + MaterialTheme.colorScheme.imageModifier + } else { + Modifier.fillMaxWidth() + } + + // Always displays Blurharh to avoid size flickering + DisplayBlurHash( + blurhash, + null, + if (isFiniteHeight) ContentScale.FillWidth else ContentScale.FillWidth, + if (ratio != null) image.aspectRatio(ratio) else modifier, + ) + if (!automaticallyStartPlayback.value) { - DisplayBlurHash( - blurhash, - null, - ContentScale.Crop, - MaterialTheme.colorScheme.imageModifier, - ) IconButton( modifier = Modifier.size(Size75dp), onClick = { automaticallyStartPlayback.value = true }, @@ -267,6 +276,7 @@ fun VideoView( title = title, thumb = thumb, roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, topPaddingForControllers = topPaddingForControllers, waveform = waveform, artworkUri = artworkUri, @@ -291,6 +301,7 @@ fun VideoViewInner( title: String? = null, thumb: VideoThumb? = null, roundedCorner: Boolean, + isFiniteHeight: Boolean, topPaddingForControllers: Dp = Dp.Unspecified, waveform: ImmutableList? = null, artworkUri: String? = null, @@ -314,6 +325,7 @@ fun VideoViewInner( controller = controller, thumbData = thumb, roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, dimensions = dimensions, blurhash = blurhash, topPaddingForControllers = topPaddingForControllers, @@ -638,6 +650,7 @@ private fun RenderVideoPlayer( controller: MediaController, thumbData: VideoThumb?, roundedCorner: Boolean, + isFiniteHeight: Boolean, dimensions: String? = null, blurhash: String? = null, topPaddingForControllers: Dp = Dp.Unspecified, @@ -653,20 +666,11 @@ private fun RenderVideoPlayer( val controllerVisible = remember(controller) { mutableStateOf(false) } - val videoPlaybackHeight = remember { mutableStateOf(Dp.Unspecified) } - - val localDensity = LocalDensity.current - - BoxWithConstraints( - modifier = - Modifier.onGloballyPositioned { coordinates -> - videoPlaybackHeight.value = with(localDensity) { coordinates.size.height.toDp() } - }, - ) { + Box { val borders = MaterialTheme.colorScheme.imageModifier val myModifier = - remember { + remember(controller) { if (roundedCorner) { modifier.then( borders.defaultMinSize(minHeight = 75.dp).align(Alignment.Center), @@ -676,17 +680,6 @@ private fun RenderVideoPlayer( } } - val ratio = remember { aspectRatio(dimensions) } - - if (ratio != null) { - DisplayBlurHash( - blurhash, - null, - ContentScale.Crop, - myModifier.aspectRatio(ratio), - ) - } - AndroidView( modifier = myModifier, factory = { context: Context -> @@ -703,7 +696,7 @@ private fun RenderVideoPlayer( thumbData?.thumb?.let { defaultArtwork = it } hideController() resizeMode = - if (maxHeight.isFinite) { + if (isFiniteHeight) { AspectRatioFrameLayout.RESIZE_MODE_FIT } else { AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH @@ -728,26 +721,9 @@ private fun RenderVideoPlayer( val startingMuteState = remember(controller) { controller.volume < 0.001 } - val topPadding = - remember { - derivedStateOf { - if (topPaddingForControllers.isSpecified && videoPlaybackHeight.value.value > 0) { - val space = (abs(this.maxHeight.value - videoPlaybackHeight.value.value) / 2).dp - if (space > topPaddingForControllers) { - Size0dp - } else { - topPaddingForControllers - space - } - } else { - Size0dp - } - } - } - MuteButton( controllerVisible, startingMuteState, - topPadding, ) { mute: Boolean -> // makes the new setting the default for new creations. DEFAULT_MUTED_SETTING.value = mute @@ -765,7 +741,6 @@ private fun RenderVideoPlayer( KeepPlayingButton( keepPlaying, controllerVisible, - topPadding, Modifier.align(Alignment.TopEnd), ) { newKeepPlaying: Boolean -> // If something else is playing and the user marks this video to keep playing, stops the other @@ -951,7 +926,6 @@ fun LayoutCoordinates.getDistanceToVertCenterIfVisible(view: View): Float? { private fun MuteButton( controllerVisible: MutableState, startingMuteState: Boolean, - topPadding: State, toggle: (Boolean) -> Unit, ) { val holdOn = @@ -972,7 +946,6 @@ private fun MuteButton( AnimatedVisibility( visible = holdOn.value || controllerVisible.value, - modifier = Modifier.padding(top = topPadding.value), enter = remember { fadeIn() }, exit = remember { fadeOut() }, ) { @@ -1005,7 +978,6 @@ private fun MuteButton( private fun KeepPlayingButton( keepPlayingStart: MutableState, controllerVisible: MutableState, - topPadding: State, alignment: Modifier, toggle: (Boolean) -> Unit, ) { @@ -1013,7 +985,7 @@ private fun KeepPlayingButton( AnimatedVisibility( visible = controllerVisible.value, - modifier = alignment.padding(top = topPadding.value), + modifier = alignment, enter = remember { fadeIn() }, exit = remember { fadeOut() }, ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 755a44508..0998e5347 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -37,22 +37,17 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.text.InlineTextContent @@ -94,7 +89,6 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.isFinite import androidx.compose.ui.unit.isSpecified import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -104,6 +98,8 @@ import androidx.core.view.ViewCompat import coil.annotation.ExperimentalCoilApi import coil.compose.AsyncImage import coil.compose.AsyncImagePainter +import coil.compose.SubcomposeAsyncImage +import coil.compose.SubcomposeAsyncImageContent import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent @@ -132,6 +128,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.toHexKey @@ -151,6 +148,7 @@ fun ZoomableContentView( content: BaseMediaContent, images: ImmutableList = remember(content) { listOf(content).toImmutableList() }, roundedCorner: Boolean, + isFiniteHeight: Boolean, accountViewModel: AccountViewModel, ) { // store the dialog open or close state @@ -189,37 +187,43 @@ fun ZoomableContentView( when (content) { is MediaUrlImage -> SensitivityWarning(content.contentWarning != null, accountViewModel) { - UrlImageView(content, mainImageModifier, accountViewModel = accountViewModel) + UrlImageView(content, mainImageModifier, isFiniteHeight, accountViewModel = accountViewModel) } is MediaUrlVideo -> SensitivityWarning(content.contentWarning != null, accountViewModel) { - VideoView( - videoUri = content.url, - title = content.description, - artworkUri = content.artworkUri, - authorName = content.authorName, - dimensions = content.dim, - blurhash = content.blurhash, - roundedCorner = roundedCorner, - nostrUriCallback = content.uri, - onDialog = { dialogOpen = true }, - accountViewModel = accountViewModel, - ) + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + VideoView( + videoUri = content.url, + title = content.description, + artworkUri = content.artworkUri, + authorName = content.authorName, + dimensions = content.dim, + blurhash = content.blurhash, + roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, + nostrUriCallback = content.uri, + onDialog = { dialogOpen = true }, + accountViewModel = accountViewModel, + ) + } } is MediaLocalImage -> - LocalImageView(content, mainImageModifier, accountViewModel = accountViewModel) + LocalImageView(content, mainImageModifier, isFiniteHeight, accountViewModel = accountViewModel) is MediaLocalVideo -> content.localFile?.let { - VideoView( - videoUri = it.toUri().toString(), - title = content.description, - artworkUri = content.artworkUri, - authorName = content.authorName, - roundedCorner = roundedCorner, - nostrUriCallback = content.uri, - onDialog = { dialogOpen = true }, - accountViewModel = accountViewModel, - ) + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + VideoView( + videoUri = it.toUri().toString(), + title = content.description, + artworkUri = content.artworkUri, + authorName = content.authorName, + roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, + nostrUriCallback = content.uri, + onDialog = { dialogOpen = true }, + accountViewModel = accountViewModel, + ) + } } } @@ -232,12 +236,13 @@ fun ZoomableContentView( private fun LocalImageView( content: MediaLocalImage, mainImageModifier: Modifier, + isFiniteHeight: Boolean, topPaddingForControllers: Dp = Dp.Unspecified, accountViewModel: AccountViewModel, alwayShowImage: Boolean = false, ) { if (content.localFileExists()) { - BoxWithConstraints(contentAlignment = Alignment.Center) { + Box(contentAlignment = Alignment.Center) { val showImage = remember { mutableStateOf( @@ -245,50 +250,85 @@ private fun LocalImageView( ) } - val myModifier = - remember { - mainImageModifier.widthIn(max = maxWidth).heightIn(max = maxHeight) - /* - .run { - aspectRatio(content.dim)?.let { ratio -> - this.aspectRatio(ratio, false) - } ?: this - } - */ - } - val contentScale = remember { - if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth + if (isFiniteHeight) ContentScale.Fit else ContentScale.FillWidth } - val verifierModifier = - if (topPaddingForControllers.isSpecified) { - Modifier.padding(top = topPaddingForControllers).align(Alignment.TopEnd) - } else { - Modifier.align(Alignment.TopEnd) - } - - val painterState = remember { mutableStateOf(null) } + val ratio = remember(content) { aspectRatio(content.dim) } if (showImage.value) { - AsyncImage( + SubcomposeAsyncImage( model = content.localFile, contentDescription = content.description, contentScale = contentScale, - modifier = myModifier, - onState = { painterState.value = it }, - ) - } + modifier = mainImageModifier, + ) { + when (painter.state) { + is AsyncImagePainter.State.Loading, + -> { + if (content.blurhash != null) { + if (ratio != null) { + DisplayBlurHash( + content.blurhash, + content.description, + contentScale, + Modifier.aspectRatio(ratio), + ) + } else { + DisplayBlurHash( + content.blurhash, + content.description, + contentScale, + Modifier, + ) + } + } else { + DisplayUrlWithLoadingSymbol(content) + } + } + is AsyncImagePainter.State.Error -> { + BlankNote() + } + is AsyncImagePainter.State.Success -> { + SubcomposeAsyncImageContent() - AddedImageFeatures( - painterState, - content, - contentScale, - myModifier, - verifierModifier, - showImage, - ) + content.isVerified?.let { + val verifierModifier = + if (topPaddingForControllers.isSpecified) { + Modifier.align(Alignment.TopEnd).padding(top = topPaddingForControllers) + } else { + Modifier.align(Alignment.TopEnd) + } + + Box(verifierModifier, contentAlignment = Alignment.TopEnd) { + HashVerificationSymbol(it) + } + } + } + else -> {} + } + } + } else { + if (content.blurhash != null && ratio != null) { + DisplayBlurHash( + content.blurhash, + content.description, + ContentScale.Crop, + mainImageModifier + .aspectRatio(ratio) + .clickable { showImage.value = true }, + ) + IconButton( + modifier = Modifier.size(Size75dp), + onClick = { showImage.value = true }, + ) { + DownloadForOfflineIcon(Size75dp, Color.White) + } + } else { + ImageUrlWithDownloadButton(content.uri, showImage) + } + } } } else { BlankNote() @@ -299,11 +339,12 @@ private fun LocalImageView( private fun UrlImageView( content: MediaUrlImage, mainImageModifier: Modifier, + isFiniteHeight: Boolean, topPaddingForControllers: Dp = Dp.Unspecified, accountViewModel: AccountViewModel, alwayShowImage: Boolean = false, ) { - BoxWithConstraints(contentAlignment = Alignment.Center) { + Box(contentAlignment = Alignment.Center) { val showImage = remember { mutableStateOf( @@ -311,42 +352,80 @@ private fun UrlImageView( ) } - val myModifier = - remember { - mainImageModifier.widthIn(max = maxWidth).heightIn(max = maxHeight) - } + val contentScale = if (isFiniteHeight) ContentScale.Fit else ContentScale.FillWidth - val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth - - val verifierModifier = - if (topPaddingForControllers.isSpecified) { - Modifier.padding(top = topPaddingForControllers).align(Alignment.TopEnd) - } else { - Modifier.align(Alignment.TopEnd) - } - - val painterState = remember { mutableStateOf(null) } + val ratio = remember(content) { aspectRatio(content.dim) } if (showImage.value) { - AsyncImage( + SubcomposeAsyncImage( model = content.url, contentDescription = content.description, contentScale = contentScale, - modifier = myModifier, - onState = { - painterState.value = it - }, - ) - } + modifier = mainImageModifier, + ) { + when (painter.state) { + is AsyncImagePainter.State.Loading, + -> { + if (content.blurhash != null) { + if (ratio != null) { + DisplayBlurHash( + content.blurhash, + content.description, + ContentScale.Crop, + Modifier.aspectRatio(ratio), + ) + } else { + DisplayBlurHash( + content.blurhash, + content.description, + ContentScale.Crop, + Modifier, + ) + } + } else { + DisplayUrlWithLoadingSymbol(content) + } + } + is AsyncImagePainter.State.Error -> { + ClickableUrl(urlText = "${content.url} ", url = content.url) + } + is AsyncImagePainter.State.Success -> { + SubcomposeAsyncImageContent() - AddedImageFeatures( - painterState, - content, - contentScale, - myModifier, - verifierModifier, - showImage, - ) + val verifierModifier = + if (topPaddingForControllers.isSpecified) { + Modifier.align(Alignment.TopEnd).padding(top = topPaddingForControllers) + } else { + Modifier.align(Alignment.TopEnd) + } + + Box(verifierModifier, contentAlignment = Alignment.TopEnd) { + ShowHash(content) + } + } + else -> {} + } + } + } else { + if (content.blurhash != null && ratio != null) { + DisplayBlurHash( + content.blurhash, + content.description, + ContentScale.Crop, + mainImageModifier + .aspectRatio(ratio) + .clickable { showImage.value = true }, + ) + IconButton( + modifier = Modifier.size(Size75dp), + onClick = { showImage.value = true }, + ) { + DownloadForOfflineIcon(Size75dp, Color.White) + } + } else { + ImageUrlWithDownloadButton(content.url, showImage) + } + } } } @@ -411,134 +490,7 @@ private fun InlineDownloadIcon(showImage: MutableState) = } @Composable -@OptIn(ExperimentalLayoutApi::class) -private fun AddedImageFeatures( - painter: MutableState, - content: MediaLocalImage, - contentScale: ContentScale, - myModifier: Modifier, - verifiedModifier: Modifier, - showImage: MutableState, -) { - val ratio = remember { aspectRatio(content.dim) } - - if (!showImage.value) { - if (content.blurhash != null && ratio != null) { - DisplayBlurHash( - content.blurhash, - content.description, - ContentScale.Crop, - myModifier.aspectRatio(ratio).clickable { showImage.value = true }, - ) - IconButton( - modifier = Modifier.size(Size75dp), - onClick = { showImage.value = true }, - ) { - DownloadForOfflineIcon(Size75dp, Color.White) - } - } else { - ImageUrlWithDownloadButton(content.uri, showImage) - } - } else { - when (painter.value) { - null, - is AsyncImagePainter.State.Loading, - -> { - if (content.blurhash != null) { - if (ratio != null) { - DisplayBlurHash( - content.blurhash, - content.description, - ContentScale.Crop, - myModifier.aspectRatio(ratio), - ) - } else { - DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier) - } - } else { - FlowRow { DisplayUrlWithLoadingSymbol(content) } - } - } - is AsyncImagePainter.State.Error -> { - BlankNote() - } - is AsyncImagePainter.State.Success -> { - content.isVerified?.let { - HashVerificationSymbol(it, verifiedModifier) - } - } - else -> {} - } - } -} - -@Composable -@OptIn(ExperimentalLayoutApi::class) -private fun AddedImageFeatures( - painter: MutableState, - content: MediaUrlImage, - contentScale: ContentScale, - myModifier: Modifier, - verifiedModifier: Modifier, - showImage: MutableState, -) { - val ratio = remember(content.url) { aspectRatio(content.dim) } - - if (!showImage.value) { - if (content.blurhash != null && ratio != null) { - DisplayBlurHash( - content.blurhash, - content.description, - ContentScale.Crop, - myModifier.aspectRatio(ratio).clickable { showImage.value = true }, - ) - IconButton( - modifier = Modifier.size(Size75dp), - onClick = { showImage.value = true }, - ) { - DownloadForOfflineIcon(Size75dp, Color.White) - } - } else { - ImageUrlWithDownloadButton(content.url, showImage) - } - } else { - when (painter.value) { - null, - is AsyncImagePainter.State.Loading, - -> { - if (content.blurhash != null) { - if (ratio != null) { - DisplayBlurHash( - content.blurhash, - content.description, - ContentScale.Crop, - myModifier.aspectRatio(ratio), - ) - } else { - DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier) - } - } else { - FlowRow(Modifier.fillMaxWidth()) { DisplayUrlWithLoadingSymbol(content) } - } - } - is AsyncImagePainter.State.Error -> { - FlowRow(Modifier.fillMaxWidth()) { - ClickableUrl(urlText = "${content.url} ", url = content.url) - } - } - is AsyncImagePainter.State.Success -> { - ShowHash(content, verifiedModifier) - } - else -> {} - } - } -} - -@Composable -fun ShowHash( - content: MediaUrlContent, - verifiedModifier: Modifier, -) { +fun ShowHash(content: MediaUrlContent) { var verifiedHash by remember(content.url) { mutableStateOf(null) } if (content.hash != null) { @@ -553,7 +505,7 @@ fun ShowHash( } } - verifiedHash?.let { HashVerificationSymbol(it, verifiedModifier) } + verifiedHash?.let { HashVerificationSymbol(it) } } fun aspectRatio(dim: String?): Float? { @@ -777,6 +729,7 @@ private fun DialogContent( RenderImageOrVideo( content = it, roundedCorner = false, + isFiniteHeight = true, topPaddingForControllers = Size55dp, onControllerVisibilityChanged = { controllerVisible.value = it }, onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value }, @@ -788,6 +741,7 @@ private fun DialogContent( RenderImageOrVideo( content = imageUrl, roundedCorner = false, + isFiniteHeight = true, topPaddingForControllers = Size55dp, onControllerVisibilityChanged = { controllerVisible.value = it }, onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value }, @@ -801,7 +755,12 @@ private fun DialogContent( exit = remember { fadeOut() }, ) { Row( - modifier = Modifier.padding(10.dp).statusBarsPadding().systemBarsPadding().fillMaxWidth(), + modifier = + Modifier + .padding(10.dp) + .statusBarsPadding() + .systemBarsPadding() + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -928,6 +887,7 @@ private fun ShareImageAction( private fun RenderImageOrVideo( content: BaseMediaContent, roundedCorner: Boolean, + isFiniteHeight: Boolean, topPaddingForControllers: Dp = Dp.Unspecified, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, onToggleControllerVisibility: (() -> Unit)? = null, @@ -937,7 +897,8 @@ private fun RenderImageOrVideo( if (content is MediaUrlImage) { val mainModifier = - Modifier.fillMaxSize() + Modifier + .fillMaxSize() .zoomable( rememberZoomState(), onTap = { @@ -950,18 +911,20 @@ private fun RenderImageOrVideo( UrlImageView( content = content, mainImageModifier = mainModifier, + isFiniteHeight = isFiniteHeight, topPaddingForControllers = topPaddingForControllers, - accountViewModel, + accountViewModel = accountViewModel, alwayShowImage = true, ) } else if (content is MediaUrlVideo) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize(1f)) { VideoViewInner( videoUri = content.url, title = content.description, artworkUri = content.artworkUri, authorName = content.authorName, roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, topPaddingForControllers = topPaddingForControllers, automaticallyStartPlayback = automaticallyStartPlayback, onControllerVisibilityChanged = onControllerVisibilityChanged, @@ -969,7 +932,8 @@ private fun RenderImageOrVideo( } } else if (content is MediaLocalImage) { val mainModifier = - Modifier.fillMaxSize() + Modifier + .fillMaxSize() .zoomable( rememberZoomState(), onTap = { @@ -982,12 +946,13 @@ private fun RenderImageOrVideo( LocalImageView( content = content, mainImageModifier = mainModifier, + isFiniteHeight = isFiniteHeight, topPaddingForControllers = topPaddingForControllers, - accountViewModel, + accountViewModel = accountViewModel, alwayShowImage = true, ) } else if (content is MediaLocalVideo) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize(1f)) { content.localFile?.let { VideoViewInner( videoUri = it.toUri().toString(), @@ -995,6 +960,7 @@ private fun RenderImageOrVideo( artworkUri = content.artworkUri, authorName = content.authorName, roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, topPaddingForControllers = topPaddingForControllers, automaticallyStartPlayback = automaticallyStartPlayback, onControllerVisibilityChanged = onControllerVisibilityChanged, @@ -1020,12 +986,8 @@ private suspend fun verifyHash(content: MediaUrlContent): Boolean? { } @Composable -private fun HashVerificationSymbol( - verifiedHash: Boolean, - modifier: Modifier, -) { +private fun HashVerificationSymbol(verifiedHash: Boolean) { val localContext = LocalContext.current - val openDialogMsg = remember { mutableStateOf(null) } openDialogMsg.value?.let { @@ -1037,25 +999,23 @@ private fun HashVerificationSymbol( } } - Box( - modifier.width(40.dp).height(40.dp).padding(10.dp), - ) { - if (verifiedHash) { - IconButton( - onClick = { - openDialogMsg.value = localContext.getString(R.string.hash_verification_passed) - }, - ) { - HashCheckIcon(Size30dp) - } - } else { - IconButton( - onClick = { - openDialogMsg.value = localContext.getString(R.string.hash_verification_failed) - }, - ) { - HashCheckFailedIcon(Size30dp) - } + if (verifiedHash) { + IconButton( + modifier = hashVerifierMark, + onClick = { + openDialogMsg.value = localContext.getString(R.string.hash_verification_passed) + }, + ) { + HashCheckIcon(Size30dp) + } + } else { + IconButton( + modifier = hashVerifierMark, + onClick = { + openDialogMsg.value = localContext.getString(R.string.hash_verification_failed) + }, + ) { + HashCheckFailedIcon(Size30dp) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt index 595a21f96..95e5bf3eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt @@ -95,6 +95,7 @@ class MarkdownMediaRenderer( ZoomableContentView( content = content, roundedCorner = true, + isFiniteHeight = false, accountViewModel = accountViewModel, ) } @@ -116,6 +117,7 @@ class MarkdownMediaRenderer( ZoomableContentView( content = content, roundedCorner = true, + isFiniteHeight = false, accountViewModel = accountViewModel, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index ef2b56c96..8d64ae0bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -29,10 +29,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -40,32 +40,28 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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.graphics.Shape import androidx.compose.ui.graphics.compositeOver -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji -import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -79,14 +75,16 @@ import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat import com.vitorpamplona.amethyst.ui.theme.RowColSpacing -import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.StdTopPadding import com.vitorpamplona.amethyst.ui.theme.chatAuthorBox -import com.vitorpamplona.amethyst.ui.theme.chatAuthorImage +import com.vitorpamplona.amethyst.ui.theme.incognitoIconModifier import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink +import com.vitorpamplona.amethyst.ui.theme.messageBubbleLimits import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.quartz.events.ChannelCreateEvent @@ -132,7 +130,6 @@ fun ChatroomMessageCompose( } } -@OptIn(ExperimentalFoundationApi::class) @Composable fun NormalChatNote( note: Note, @@ -145,6 +142,17 @@ fun NormalChatNote( onWantsToReply: (Note) -> Unit, onWantsToEditDraft: (Note) -> Unit, ) { + val isLoggedInUser = + remember(note.author) { + accountViewModel.isLoggedUser(note.author) + } + + if (routeForLastRead != null) { + LaunchedEffect(key1 = routeForLastRead) { + accountViewModel.loadAndMarkAsRead(routeForLastRead, note.createdAt()) + } + } + val drawAuthorInfo by remember(note) { derivedStateOf { @@ -162,13 +170,104 @@ fun NormalChatNote( } } + ChatBubbleLayout( + isLoggedInUser = isLoggedInUser, + innerQuote = innerQuote, + isSimplified = accountViewModel.settings.featureSet == FeatureSetType.SIMPLIFIED, + hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(), + drawAuthorInfo = drawAuthorInfo, + parentBackgroundColor = parentBackgroundColor, + onClick = { + if (note.event is ChannelCreateEvent) { + nav("Channel/${note.idHex}") + true + } else { + false + } + }, + onAuthorClick = { + note.author?.let { + nav("User/${it.pubkeyHex}") + } + }, + actionMenu = { popupExpanded, onDismiss -> + NoteQuickActionMenu( + note = note, + popupExpanded = popupExpanded, + onDismiss = onDismiss, + onWantsToEditDraft = { onWantsToEditDraft(note) }, + accountViewModel = accountViewModel, + nav = nav, + ) + }, + drawAuthorLine = { + DrawAuthorInfo( + note, + accountViewModel, + nav, + ) + }, + detailRow = { + if (note.isDraft()) { + DisplayDraftChat() + } + IncognitoBadge(note) + ChatTimeAgo(note) + RelayBadgesHorizontal(note, accountViewModel, nav = nav) + Spacer(modifier = DoubleHorzSpacer) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) { + ReplyReaction( + baseNote = note, + grayTint = MaterialTheme.colorScheme.placeholderText, + accountViewModel = accountViewModel, + showCounter = false, + iconSizeModifier = Size15Modifier, + ) { + onWantsToReply(note) + } + Spacer(modifier = StdHorzSpacer) + LikeReaction(note, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav) + + ZapReaction(note, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav) + } + }, + ) { backgroundBubbleColor -> + MessageBubbleLines( + note, + innerQuote, + backgroundBubbleColor, + onWantsToReply, + onWantsToEditDraft, + canPreview, + accountViewModel, + nav, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ChatBubbleLayout( + isLoggedInUser: Boolean, + innerQuote: Boolean, + isSimplified: Boolean, + hasDetailsToShow: Boolean, + drawAuthorInfo: Boolean, + parentBackgroundColor: MutableState? = null, + onClick: () -> Boolean, + onAuthorClick: () -> Unit, + actionMenu: @Composable (popupExpanded: Boolean, onDismiss: () -> Unit) -> Unit, + detailRow: @Composable () -> Unit, + drawAuthorLine: @Composable () -> Unit, + inner: @Composable (MutableState) -> Unit, +) { val loggedInColors = MaterialTheme.colorScheme.mediumImportanceLink val otherColors = MaterialTheme.colorScheme.subtleBorder val defaultBackground = MaterialTheme.colorScheme.background val backgroundBubbleColor = remember { - if (accountViewModel.isLoggedUser(note.author)) { + if (isLoggedInUser) { mutableStateOf( loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground), ) @@ -176,182 +275,176 @@ fun NormalChatNote( mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground)) } } + val alignment: Arrangement.Horizontal = - remember { - if (accountViewModel.isLoggedUser(note.author)) { - Arrangement.End - } else { - Arrangement.Start - } + if (isLoggedInUser) { + Arrangement.End + } else { + Arrangement.Start } + val shape: Shape = - remember { - if (accountViewModel.isLoggedUser(note.author)) { - ChatBubbleShapeMe - } else { - ChatBubbleShapeThem + if (isLoggedInUser) { + ChatBubbleShapeMe + } else { + ChatBubbleShapeThem + } + + Row( + modifier = if (innerQuote) ChatPaddingInnerQuoteModifier else ChatPaddingModifier, + horizontalArrangement = alignment, + ) { + var popupExpanded by remember { mutableStateOf(false) } + + val modif2 = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier + + val showDetails = + remember { + mutableStateOf( + if (isSimplified) { + hasDetailsToShow + } else { + true + }, + ) } - } - if (routeForLastRead != null) { - LaunchedEffect(key1 = routeForLastRead) { - accountViewModel.loadAndMarkAsRead(routeForLastRead, note.createdAt()) - } - } - - Column { - Row( - modifier = if (innerQuote) ChatPaddingInnerQuoteModifier else ChatPaddingModifier, - horizontalArrangement = alignment, - ) { - val availableBubbleSize = remember { mutableIntStateOf(0) } - var popupExpanded by remember { mutableStateOf(false) } - - val modif2 = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier - - val showDetails = - remember { - mutableStateOf( - if (accountViewModel.settings.featureSet == FeatureSetType.SIMPLIFIED) { - note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty() - } else { - true - }, - ) - } - - val clickableModifier = - remember { - Modifier.combinedClickable( - onClick = { - if (note.event is ChannelCreateEvent) { - nav("Channel/${note.idHex}") - } else { - if (accountViewModel.settings.featureSet == FeatureSetType.SIMPLIFIED) { - showDetails.value = !showDetails.value - } + val clickableModifier = + remember { + Modifier.combinedClickable( + onClick = { + if (!onClick()) { + if (isSimplified) { + showDetails.value = !showDetails.value } - }, - onLongClick = { popupExpanded = true }, - ) - } - - Row( - horizontalArrangement = alignment, - modifier = - modif2.onSizeChanged { - if (availableBubbleSize.intValue != it.width) { - availableBubbleSize.intValue = it.width } }, - ) { - Surface( - color = backgroundBubbleColor.value, - shape = shape, - modifier = clickableModifier, - ) { - RenderBubble( - note, - drawAuthorInfo, - alignment, - innerQuote, - backgroundBubbleColor, - onWantsToReply, - onWantsToEditDraft, - canPreview, - availableBubbleSize, - showDetails, - accountViewModel, - nav, - ) - } + onLongClick = { popupExpanded = true }, + ) } - NoteQuickActionMenu( - note = note, - popupExpanded = popupExpanded, - onDismiss = { popupExpanded = false }, - onWantsToEditDraft = { onWantsToEditDraft(note) }, - accountViewModel = accountViewModel, - nav = nav, - ) + Row( + horizontalArrangement = alignment, + modifier = modif2, + ) { + Surface( + color = backgroundBubbleColor.value, + shape = shape, + modifier = clickableModifier, + ) { + Column(modifier = messageBubbleLimits, verticalArrangement = RowColSpacing5dp) { + if (drawAuthorInfo) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = alignment, + modifier = StdTopPadding.then(Modifier.clickable(onClick = onAuthorClick)), + ) { + drawAuthorLine() + } + } + + inner(backgroundBubbleColor) + + if (showDetails.value) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = ReactionRowHeightChat, + ) { + detailRow() + } + } + } + } + } + + actionMenu(popupExpanded) { + popupExpanded = false } } } +@Preview @Composable -private fun RenderBubble( - baseNote: Note, - drawAuthorInfo: Boolean, - alignment: Arrangement.Horizontal, - innerQuote: Boolean, - backgroundBubbleColor: MutableState, - onWantsToReply: (Note) -> Unit, - onWantsToEditDraft: (Note) -> Unit, - canPreview: Boolean, - availableBubbleSize: MutableState, - showDetails: State, - accountViewModel: AccountViewModel, - nav: (String) -> Unit, -) { - val bubbleSize = remember { mutableIntStateOf(0) } - - val bubbleModifier = +private fun BubblePreview() { + val backgroundBubbleColor = remember { - Modifier - .padding(start = 10.dp, end = 10.dp, bottom = 5.dp) - .onSizeChanged { - if (bubbleSize.intValue != it.width) { - bubbleSize.intValue = it.width - } - } + mutableStateOf(Color.Transparent) } - Column(modifier = bubbleModifier) { - MessageBubbleLines( - drawAuthorInfo, - baseNote, - alignment, - availableBubbleSize, - innerQuote, - backgroundBubbleColor, - bubbleSize, - onWantsToReply, - onWantsToEditDraft, - canPreview, - showDetails, - accountViewModel, - nav, - ) + Column { + ChatBubbleLayout( + isLoggedInUser = false, + innerQuote = false, + isSimplified = false, + hasDetailsToShow = true, + drawAuthorInfo = true, + parentBackgroundColor = backgroundBubbleColor, + onClick = { false }, + onAuthorClick = {}, + actionMenu = { popupExpanded, onDismiss -> + }, + drawAuthorLine = { + UserDisplayNameLayout( + picture = { + Icon( + imageVector = Icons.Default.Person, + contentDescription = null, + modifier = Modifier.size(Size20dp).clip(CircleShape), + ) + }, + name = { + Text("Me", fontWeight = FontWeight.Bold) + }, + ) + }, + detailRow = { Text("Relays and Actions") }, + ) { backgroundBubbleColor -> + Text("This is my note") + } + + ChatBubbleLayout( + isLoggedInUser = true, + innerQuote = false, + isSimplified = false, + hasDetailsToShow = true, + drawAuthorInfo = true, + parentBackgroundColor = backgroundBubbleColor, + onClick = { false }, + onAuthorClick = {}, + actionMenu = { popupExpanded, onDismiss -> + }, + drawAuthorLine = { + UserDisplayNameLayout( + picture = { + Icon( + imageVector = Icons.Default.Person, + contentDescription = null, + modifier = Modifier.size(Size20dp).clip(CircleShape), + ) + }, + name = { + Text("Me", fontWeight = FontWeight.Bold) + }, + ) + }, + detailRow = { Text("Relays and Actions") }, + ) { backgroundBubbleColor -> + Text("This is a very long long loong note") + } } } @Composable private fun MessageBubbleLines( - drawAuthorInfo: Boolean, baseNote: Note, - alignment: Arrangement.Horizontal, - availableBubbleSize: MutableState, innerQuote: Boolean, backgroundBubbleColor: MutableState, - bubbleSize: MutableState, onWantsToReply: (Note) -> Unit, onWantsToEditDraft: (Note) -> Unit, canPreview: Boolean, - showDetails: State, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - if (drawAuthorInfo) { - DrawAuthorInfo( - baseNote, - alignment, - accountViewModel.settings.showProfilePictures.value, - accountViewModel, - nav, - ) - } - if (baseNote.event !is DraftEvent) { RenderReplyRow( note = baseNote, @@ -374,41 +467,6 @@ private fun MessageBubbleLines( accountViewModel = accountViewModel, nav = nav, ) - - if (showDetails.value) { - ConstrainedStatusRow( - bubbleSize = bubbleSize, - availableBubbleSize = availableBubbleSize, - firstColumn = { - if (baseNote.isDraft()) { - DisplayDraftChat() - } - IncognitoBadge(baseNote) - ChatTimeAgo(baseNote) - RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav) - Spacer(modifier = DoubleHorzSpacer) - }, - secondColumn = { - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) { - LikeReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav) - } - ZapReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav) - Spacer(modifier = StdHorzSpacer) - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) { - ReplyReaction( - baseNote = baseNote, - grayTint = MaterialTheme.colorScheme.placeholderText, - accountViewModel = accountViewModel, - showCounter = false, - iconSizeModifier = Size15Modifier, - ) { - onWantsToReply(baseNote) - } - } - Spacer(modifier = StdHorzSpacer) - }, - ) - } } @Composable @@ -536,43 +594,9 @@ private fun RenderDraftEvent( @Composable private fun ConstrainedStatusRow( - bubbleSize: MutableState, - availableBubbleSize: MutableState, firstColumn: @Composable () -> Unit, secondColumn: @Composable () -> Unit, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = - with(LocalDensity.current) { - Modifier - .padding(top = Size5dp) - .height(Size20dp) - .widthIn( - bubbleSize.value.toDp(), - availableBubbleSize.value.toDp(), - ) - }, - ) { - Column(modifier = ReactionRowHeightChat) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = ReactionRowHeightChat, - ) { - firstColumn() - } - } - - Column(modifier = ReactionRowHeightChat) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = ReactionRowHeightChat, - ) { - secondColumn() - } - } - } } @Composable @@ -703,68 +727,56 @@ private fun RenderCreateChannelNote(note: Note) { @Composable private fun DrawAuthorInfo( baseNote: Note, - alignment: Arrangement.Horizontal, - loadProfilePicture: Boolean, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { baseNote.author?.let { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = alignment, - modifier = - Modifier - .padding(top = Size10dp) - .clickable { - nav("User/${baseNote.author?.pubkeyHex}") - }, - ) { - WatchAndDisplayUser(it, loadProfilePicture, accountViewModel, nav) - } + WatchAndDisplayUser(it, accountViewModel, nav) } } +@Composable +fun UserDisplayNameLayout( + picture: @Composable () -> Unit, + name: @Composable () -> Unit, +) { + Box(chatAuthorBox, contentAlignment = Alignment.TopEnd) { + picture() + } + + Spacer(modifier = StdHorzSpacer) + + name() +} + @Composable private fun WatchAndDisplayUser( author: User, - loadProfilePicture: Boolean, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { val userState by author.live().userMetadataInfo.observeAsState() - Box(chatAuthorBox, contentAlignment = Alignment.TopEnd) { - InnerUserPicture( - userHex = author.pubkeyHex, - userPicture = userState?.picture, - userName = userState?.bestName(), - size = Size20dp, - modifier = Modifier, - accountViewModel = accountViewModel, - ) + UserDisplayNameLayout( + picture = { + InnerUserPicture( + userHex = author.pubkeyHex, + userPicture = userState?.picture, + userName = userState?.bestName(), + size = Size20dp, + modifier = Modifier, + accountViewModel = accountViewModel, + ) - ObserveAndDisplayFollowingMark(author.pubkeyHex, Size5dp, accountViewModel) - } - - if (userState != null) { - DisplayMessageUsername(userState?.bestName() ?: author.pubkeyDisplayHex(), userState?.tags ?: EmptyTagList) - } else { - DisplayMessageUsername(author.pubkeyDisplayHex(), EmptyTagList) - } -} - -@Composable -private fun UserIcon( - pubkeyHex: String, - userProfilePicture: String?, - loadProfilePicture: Boolean, -) { - RobohashFallbackAsyncImage( - robot = pubkeyHex, - model = userProfilePicture, - contentDescription = stringResource(id = R.string.profile_image), - loadProfilePicture = loadProfilePicture, - modifier = chatAuthorImage, + ObserveAndDisplayFollowingMark(author.pubkeyHex, Size5dp, accountViewModel) + }, + name = { + if (userState != null) { + DisplayMessageUsername(userState?.bestName() ?: author.pubkeyDisplayHex(), userState?.tags ?: EmptyTagList) + } else { + DisplayMessageUsername(author.pubkeyDisplayHex(), EmptyTagList) + } + }, ) } @@ -773,7 +785,6 @@ private fun DisplayMessageUsername( userDisplayName: String, userTags: ImmutableListOfLists, ) { - Spacer(modifier = StdHorzSpacer) CreateTextWithEmoji( text = userDisplayName, tags = userTags, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index e1ba673d3..5bcc4f7d9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -303,8 +303,8 @@ fun AcceptableNote( ) } is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote) - is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel) - is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel) + is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, false, accountViewModel) + is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, false, accountViewModel) else -> LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> CheckNewAndRenderNote( @@ -587,8 +587,8 @@ private fun RenderNoteRow( val noteEvent = baseNote.event when (noteEvent) { is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav) - is AudioTrackEvent -> RenderAudioTrack(baseNote, accountViewModel, nav) - is AudioHeaderEvent -> RenderAudioHeader(baseNote, accountViewModel, nav) + is AudioTrackEvent -> RenderAudioTrack(baseNote, false, accountViewModel, nav) + is AudioHeaderEvent -> RenderAudioHeader(baseNote, false, accountViewModel, nav) is DraftEvent -> RenderDraft(baseNote, quotesLeft, unPackReply, backgroundColor, accountViewModel, nav) is ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) @@ -706,10 +706,10 @@ private fun RenderNoteRow( nav, ) } - is FileHeaderEvent -> FileHeaderDisplay(baseNote, true, accountViewModel) - is VideoHorizontalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav) - is VideoVerticalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav) - is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, accountViewModel) + is FileHeaderEvent -> FileHeaderDisplay(baseNote, true, false, accountViewModel) + is VideoHorizontalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, false, accountViewModel, nav) + is VideoVerticalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, false, accountViewModel, nav) + is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, false, accountViewModel) is CommunityPostApprovalEvent -> { RenderPostApproval( baseNote, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index ad633cb44..db48f8f6e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -60,18 +60,20 @@ import java.util.Locale @Composable fun RenderAudioTrack( note: Note, + isFiniteHeight: Boolean, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { val noteEvent = note.event as? AudioTrackEvent ?: return - AudioTrackHeader(noteEvent, note, accountViewModel, nav) + AudioTrackHeader(noteEvent, note, isFiniteHeight, accountViewModel, nav) } @Composable fun AudioTrackHeader( noteEvent: AudioTrackEvent, note: Note, + isFiniteHeight: Boolean, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -144,17 +146,20 @@ fun AudioTrackHeader( thumbUri = cover, authorName = note.author?.toBestDisplayName(), roundedCorner = true, + isFiniteHeight = isFiniteHeight, nostrUriCallback = "nostr:${note.toNEvent()}", accountViewModel = accountViewModel, ) - } - ?: VideoView( + } ?: run { + VideoView( videoUri = media, title = noteEvent.subject(), authorName = note.author?.toBestDisplayName(), roundedCorner = true, + isFiniteHeight = isFiniteHeight, accountViewModel = accountViewModel, ) + } } } } @@ -164,18 +169,20 @@ fun AudioTrackHeader( @Composable fun RenderAudioHeader( note: Note, + isFiniteHeight: Boolean, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { val noteEvent = note.event as? AudioHeaderEvent ?: return - AudioHeader(noteEvent, note, accountViewModel, nav) + AudioHeader(noteEvent, note, isFiniteHeight, accountViewModel, nav) } @Composable fun AudioHeader( noteEvent: AudioHeaderEvent, note: Note, + isFiniteHeight: Boolean, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -199,6 +206,7 @@ fun AudioHeader( title = noteEvent.subject(), authorName = note.author?.toBestDisplayName(), roundedCorner = true, + isFiniteHeight = isFiniteHeight, accountViewModel = accountViewModel, nostrUriCallback = note.toNostrUri(), ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt index 2a8864fe9..298ed1e1a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent fun FileHeaderDisplay( note: Note, roundedCorner: Boolean, + isFiniteHeight: Boolean, accountViewModel: AccountViewModel, ) { val event = (note.event as? FileHeaderEvent) ?: return @@ -80,6 +81,7 @@ fun FileHeaderDisplay( ZoomableContentView( content = content, roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, accountViewModel = accountViewModel, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt index aeeb753a3..26eab2f87 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt @@ -42,6 +42,7 @@ import java.io.File fun FileStorageHeaderDisplay( baseNote: Note, roundedCorner: Boolean, + isFiniteHeight: Boolean, accountViewModel: AccountViewModel, ) { val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return @@ -49,7 +50,7 @@ fun FileStorageHeaderDisplay( LoadNote(baseNoteHex = dataEventId, accountViewModel) { contentNote -> if (contentNote != null) { - ObserverAndRenderNIP95(baseNote, contentNote, roundedCorner, accountViewModel) + ObserverAndRenderNIP95(baseNote, contentNote, roundedCorner, isFiniteHeight, accountViewModel) } } } @@ -59,6 +60,7 @@ private fun ObserverAndRenderNIP95( header: Note, content: Note, roundedCorner: Boolean, + isFiniteHeight: Boolean, accountViewModel: AccountViewModel, ) { val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return @@ -110,6 +112,7 @@ private fun ObserverAndRenderNIP95( ZoomableContentView( content = it, roundedCorner = roundedCorner, + isFiniteHeight = isFiniteHeight, accountViewModel = accountViewModel, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index 36f25aaed..bc362d984 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -156,6 +156,7 @@ fun RenderLiveActivityEventInner( artworkUri = cover, authorName = baseNote.author?.toBestDisplayName(), roundedCorner = true, + isFiniteHeight = false, accountViewModel = accountViewModel, nostrUriCallback = "nostr:${baseNote.toNEvent()}", ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index a6e473522..c779b561f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -66,6 +66,7 @@ fun VideoDisplay( makeItShort: Boolean, canPreview: Boolean, backgroundColor: MutableState, + isFiniteHeight: Boolean, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -143,6 +144,7 @@ fun VideoDisplay( ZoomableContentView( content = content, roundedCorner = true, + isFiniteHeight = isFiniteHeight, accountViewModel = accountViewModel, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 0de0df8c8..24d9c83e8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -504,17 +504,17 @@ fun NoteMaster( nav = nav, ) } else if (noteEvent is VideoEvent) { - VideoDisplay(baseNote, false, true, backgroundColor, accountViewModel, nav) + VideoDisplay(baseNote, false, true, backgroundColor, false, accountViewModel, nav) } else if (noteEvent is FileHeaderEvent) { - FileHeaderDisplay(baseNote, true, accountViewModel) + FileHeaderDisplay(baseNote, true, false, accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { - FileStorageHeaderDisplay(baseNote, true, accountViewModel) + FileStorageHeaderDisplay(baseNote, true, false, accountViewModel) } else if (noteEvent is PeopleListEvent) { DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is AudioTrackEvent) { - AudioTrackHeader(noteEvent, baseNote, accountViewModel, nav) + AudioTrackHeader(noteEvent, baseNote, false, accountViewModel, nav) } else if (noteEvent is AudioHeaderEvent) { - AudioHeader(noteEvent, baseNote, accountViewModel, nav) + AudioHeader(noteEvent, baseNote, false, accountViewModel, nav) } else if (noteEvent is CommunityPostApprovalEvent) { RenderPostApproval( baseNote, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index da426cd04..7648f7244 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -743,6 +743,7 @@ fun ShowVideoStreaming( ZoomableContentView( content = zoomableUrlVideo, roundedCorner = false, + isFiniteHeight = false, accountViewModel = accountViewModel, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index c37b11250..8b010ec6e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -86,7 +86,7 @@ fun ChatroomListScreen( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - val windowSizeClass = accountViewModel.settings.windowSizeClass.value + val windowSizeClass by accountViewModel.settings.windowSizeClass val twoPane by remember { derivedStateOf { @@ -104,7 +104,7 @@ fun ChatroomListScreen( ChatroomListTwoPane( knownFeedViewModel = knownFeedViewModel, newFeedViewModel = newFeedViewModel, - widthSizeClass = windowSizeClass.widthSizeClass, + widthSizeClass = windowSizeClass!!.widthSizeClass, accountViewModel = accountViewModel, nav = nav, ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index 2270fd09a..6c084f725 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -325,12 +325,12 @@ private fun RenderVideoOrPictureNote( nav: (String) -> Unit, ) { Column(Modifier.fillMaxSize(1f), verticalArrangement = Arrangement.Center) { - Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { + Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { val noteEvent = remember { note.event } if (noteEvent is FileHeaderEvent) { - FileHeaderDisplay(note, false, accountViewModel) + FileHeaderDisplay(note, false, true, accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { - FileStorageHeaderDisplay(note, false, accountViewModel) + FileStorageHeaderDisplay(note, false, true, accountViewModel) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index f79265eeb..250480db1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -108,6 +108,7 @@ val StdPadding = Modifier.padding(10.dp) val BigPadding = Modifier.padding(15.dp) val RowColSpacing = Arrangement.spacedBy(3.dp) +val RowColSpacing5dp = Arrangement.spacedBy(5.dp) val HalfHorzPadding = Modifier.padding(horizontal = 5.dp) val HalfVertPadding = Modifier.padding(vertical = 5.dp) @@ -236,6 +237,9 @@ val chatAuthorBox = Modifier.size(20.dp) val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape) val AuthorInfoVideoFeed = Modifier.width(75.dp).padding(end = 15.dp) +val messageDetailsModifier = Modifier.height(Size25dp) +val messageBubbleLimits = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 5.dp) + val inlinePlaceholder = Placeholder( width = Font17SP, @@ -247,3 +251,5 @@ val incognitoIconModifier = Modifier .padding(top = 1.dp) .size(14.dp) + +val hashVerifierMark = Modifier.width(40.dp).height(40.dp).padding(10.dp)