- Performance Improvements: Simplifies intrinsic size calculations for Image Previews and Videos

- Improves rendering of chat messages
- Fixes the jittering from resizing Videos and Images during loading.
- Fixes video cut off when loading
- Fixes blurhash covering videos in landscape
- Fixes landscape video centralization
- Fixes chat using Two Pane layout for landscape and regular for portrait
This commit is contained in:
Vitor Pamplona
2024-06-04 15:45:59 -04:00
parent 16142f61b8
commit a6a8138c7f
21 changed files with 589 additions and 612 deletions
@@ -45,6 +45,10 @@ fun prepareSharedViewModel(act: MainActivity): SharedPreferencesViewModel {
sharedPreferencesViewModel.updateDisplaySettings(windowSizeClass, displayFeatures) sharedPreferencesViewModel.updateDisplaySettings(windowSizeClass, displayFeatures)
} }
LaunchedEffect(key1 = displayFeatures, key2 = windowSizeClass) {
sharedPreferencesViewModel.updateDisplaySettings(windowSizeClass, displayFeatures)
}
LaunchedEffect(act.isOnMobileDataState) { LaunchedEffect(act.isOnMobileDataState) {
sharedPreferencesViewModel.updateConnectivityStatusState(act.isOnMobileDataState) sharedPreferencesViewModel.updateConnectivityStatusState(act.isOnMobileDataState)
} }
@@ -305,6 +305,7 @@ fun EditPostView(
VideoView( VideoView(
myUrlPreview, myUrlPreview,
roundedCorner = true, roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} else { } else {
@@ -252,6 +252,7 @@ fun ImageVideoPost(
VideoView( VideoView(
videoUri = it.toString(), videoUri = it.toString(),
roundedCorner = false, roundedCorner = false,
isFiniteHeight = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} }
@@ -426,6 +426,7 @@ fun NewPostView(
VideoView( VideoView(
myUrlPreview, myUrlPreview,
roundedCorner = true, roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} else { } else {
@@ -1736,7 +1737,7 @@ fun ImageVideoDescription(
) )
} }
} else { } else {
VideoView(uri.toString(), roundedCorner = true, accountViewModel = accountViewModel) VideoView(uri.toString(), roundedCorner = true, isFiniteHeight = false, accountViewModel = accountViewModel)
} }
} }
@@ -84,6 +84,7 @@ fun RenderLoaded(
ZoomableContentView( ZoomableContentView(
content = MediaUrlImage(url, uri = callbackUri), content = MediaUrlImage(url, uri = callbackUri),
roundedCorner = true, roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} }
@@ -92,6 +93,7 @@ fun RenderLoaded(
ZoomableContentView( ZoomableContentView(
content = MediaUrlVideo(url, uri = callbackUri), content = MediaUrlVideo(url, uri = callbackUri),
roundedCorner = true, roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} }
@@ -446,7 +446,7 @@ private fun ZoomableContentView(
) { ) {
state.imagesForPager[word]?.let { state.imagesForPager[word]?.let {
Box(modifier = HalfVertPadding) { Box(modifier = HalfVertPadding) {
ZoomableContentView(it, state.imageList, roundedCorner = true, accountViewModel) ZoomableContentView(it, state.imageList, roundedCorner = true, isFiniteHeight = false, accountViewModel)
} }
} }
} }
@@ -36,7 +36,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -53,7 +52,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf 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.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver 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.note.MutedIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize 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.Size22Modifier
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.Size75dp
@@ -127,6 +121,7 @@ fun LoadThumbAndThenVideoView(
thumbUri: String, thumbUri: String,
authorName: String? = null, authorName: String? = null,
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean,
nostrUriCallback: String? = null, nostrUriCallback: String? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onDialog: ((Boolean) -> Unit)? = null, onDialog: ((Boolean) -> Unit)? = null,
@@ -156,6 +151,7 @@ fun LoadThumbAndThenVideoView(
title = title, title = title,
thumb = VideoThumb(loadingFinished.second), thumb = VideoThumb(loadingFinished.second),
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
artworkUri = thumbUri, artworkUri = thumbUri,
authorName = authorName, authorName = authorName,
nostrUriCallback = nostrUriCallback, nostrUriCallback = nostrUriCallback,
@@ -168,6 +164,7 @@ fun LoadThumbAndThenVideoView(
title = title, title = title,
thumb = null, thumb = null,
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
artworkUri = thumbUri, artworkUri = thumbUri,
authorName = authorName, authorName = authorName,
nostrUriCallback = nostrUriCallback, nostrUriCallback = nostrUriCallback,
@@ -184,6 +181,7 @@ fun VideoView(
title: String? = null, title: String? = null,
thumb: VideoThumb? = null, thumb: VideoThumb? = null,
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean,
topPaddingForControllers: Dp = Dp.Unspecified, topPaddingForControllers: Dp = Dp.Unspecified,
waveform: ImmutableList<Int>? = null, waveform: ImmutableList<Int>? = null,
artworkUri: String? = null, artworkUri: String? = null,
@@ -208,7 +206,7 @@ fun VideoView(
if (blurhash == null) { if (blurhash == null) {
val ratio = aspectRatio(dimensions) val ratio = aspectRatio(dimensions)
val modifier = val modifier =
if (ratio != null && roundedCorner && automaticallyStartPlayback.value) { if (ratio != null && automaticallyStartPlayback.value) {
Modifier.aspectRatio(ratio) Modifier.aspectRatio(ratio)
} else { } else {
Modifier Modifier
@@ -224,6 +222,7 @@ fun VideoView(
title = title, title = title,
thumb = thumb, thumb = thumb,
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
topPaddingForControllers = topPaddingForControllers, topPaddingForControllers = topPaddingForControllers,
waveform = waveform, waveform = waveform,
artworkUri = artworkUri, artworkUri = artworkUri,
@@ -239,21 +238,31 @@ fun VideoView(
} }
} else { } else {
val ratio = aspectRatio(dimensions) val ratio = aspectRatio(dimensions)
val modifier = val modifier =
if (ratio != null && roundedCorner) { if (ratio != null) {
Modifier.aspectRatio(ratio) Modifier.aspectRatio(ratio)
} else { } else {
Modifier Modifier
} }
Box(modifier, contentAlignment = Alignment.Center) { 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) { if (!automaticallyStartPlayback.value) {
DisplayBlurHash(
blurhash,
null,
ContentScale.Crop,
MaterialTheme.colorScheme.imageModifier,
)
IconButton( IconButton(
modifier = Modifier.size(Size75dp), modifier = Modifier.size(Size75dp),
onClick = { automaticallyStartPlayback.value = true }, onClick = { automaticallyStartPlayback.value = true },
@@ -267,6 +276,7 @@ fun VideoView(
title = title, title = title,
thumb = thumb, thumb = thumb,
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
topPaddingForControllers = topPaddingForControllers, topPaddingForControllers = topPaddingForControllers,
waveform = waveform, waveform = waveform,
artworkUri = artworkUri, artworkUri = artworkUri,
@@ -291,6 +301,7 @@ fun VideoViewInner(
title: String? = null, title: String? = null,
thumb: VideoThumb? = null, thumb: VideoThumb? = null,
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean,
topPaddingForControllers: Dp = Dp.Unspecified, topPaddingForControllers: Dp = Dp.Unspecified,
waveform: ImmutableList<Int>? = null, waveform: ImmutableList<Int>? = null,
artworkUri: String? = null, artworkUri: String? = null,
@@ -314,6 +325,7 @@ fun VideoViewInner(
controller = controller, controller = controller,
thumbData = thumb, thumbData = thumb,
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
dimensions = dimensions, dimensions = dimensions,
blurhash = blurhash, blurhash = blurhash,
topPaddingForControllers = topPaddingForControllers, topPaddingForControllers = topPaddingForControllers,
@@ -638,6 +650,7 @@ private fun RenderVideoPlayer(
controller: MediaController, controller: MediaController,
thumbData: VideoThumb?, thumbData: VideoThumb?,
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean,
dimensions: String? = null, dimensions: String? = null,
blurhash: String? = null, blurhash: String? = null,
topPaddingForControllers: Dp = Dp.Unspecified, topPaddingForControllers: Dp = Dp.Unspecified,
@@ -653,20 +666,11 @@ private fun RenderVideoPlayer(
val controllerVisible = remember(controller) { mutableStateOf(false) } val controllerVisible = remember(controller) { mutableStateOf(false) }
val videoPlaybackHeight = remember { mutableStateOf<Dp>(Dp.Unspecified) } Box {
val localDensity = LocalDensity.current
BoxWithConstraints(
modifier =
Modifier.onGloballyPositioned { coordinates ->
videoPlaybackHeight.value = with(localDensity) { coordinates.size.height.toDp() }
},
) {
val borders = MaterialTheme.colorScheme.imageModifier val borders = MaterialTheme.colorScheme.imageModifier
val myModifier = val myModifier =
remember { remember(controller) {
if (roundedCorner) { if (roundedCorner) {
modifier.then( modifier.then(
borders.defaultMinSize(minHeight = 75.dp).align(Alignment.Center), 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( AndroidView(
modifier = myModifier, modifier = myModifier,
factory = { context: Context -> factory = { context: Context ->
@@ -703,7 +696,7 @@ private fun RenderVideoPlayer(
thumbData?.thumb?.let { defaultArtwork = it } thumbData?.thumb?.let { defaultArtwork = it }
hideController() hideController()
resizeMode = resizeMode =
if (maxHeight.isFinite) { if (isFiniteHeight) {
AspectRatioFrameLayout.RESIZE_MODE_FIT AspectRatioFrameLayout.RESIZE_MODE_FIT
} else { } else {
AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
@@ -728,26 +721,9 @@ private fun RenderVideoPlayer(
val startingMuteState = remember(controller) { controller.volume < 0.001 } 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( MuteButton(
controllerVisible, controllerVisible,
startingMuteState, startingMuteState,
topPadding,
) { mute: Boolean -> ) { mute: Boolean ->
// makes the new setting the default for new creations. // makes the new setting the default for new creations.
DEFAULT_MUTED_SETTING.value = mute DEFAULT_MUTED_SETTING.value = mute
@@ -765,7 +741,6 @@ private fun RenderVideoPlayer(
KeepPlayingButton( KeepPlayingButton(
keepPlaying, keepPlaying,
controllerVisible, controllerVisible,
topPadding,
Modifier.align(Alignment.TopEnd), Modifier.align(Alignment.TopEnd),
) { newKeepPlaying: Boolean -> ) { newKeepPlaying: Boolean ->
// If something else is playing and the user marks this video to keep playing, stops the other // 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( private fun MuteButton(
controllerVisible: MutableState<Boolean>, controllerVisible: MutableState<Boolean>,
startingMuteState: Boolean, startingMuteState: Boolean,
topPadding: State<Dp>,
toggle: (Boolean) -> Unit, toggle: (Boolean) -> Unit,
) { ) {
val holdOn = val holdOn =
@@ -972,7 +946,6 @@ private fun MuteButton(
AnimatedVisibility( AnimatedVisibility(
visible = holdOn.value || controllerVisible.value, visible = holdOn.value || controllerVisible.value,
modifier = Modifier.padding(top = topPadding.value),
enter = remember { fadeIn() }, enter = remember { fadeIn() },
exit = remember { fadeOut() }, exit = remember { fadeOut() },
) { ) {
@@ -1005,7 +978,6 @@ private fun MuteButton(
private fun KeepPlayingButton( private fun KeepPlayingButton(
keepPlayingStart: MutableState<Boolean>, keepPlayingStart: MutableState<Boolean>,
controllerVisible: MutableState<Boolean>, controllerVisible: MutableState<Boolean>,
topPadding: State<Dp>,
alignment: Modifier, alignment: Modifier,
toggle: (Boolean) -> Unit, toggle: (Boolean) -> Unit,
) { ) {
@@ -1013,7 +985,7 @@ private fun KeepPlayingButton(
AnimatedVisibility( AnimatedVisibility(
visible = controllerVisible.value, visible = controllerVisible.value,
modifier = alignment.padding(top = topPadding.value), modifier = alignment,
enter = remember { fadeIn() }, enter = remember { fadeIn() },
exit = remember { fadeOut() }, exit = remember { fadeOut() },
) { ) {
@@ -37,22 +37,17 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.text.InlineTextContent 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.text.withStyle
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.unit.isSpecified
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@@ -104,6 +98,8 @@ import androidx.core.view.ViewCompat
import coil.annotation.ExperimentalCoilApi import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter import coil.compose.AsyncImagePainter
import coil.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent 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.Size5dp
import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
@@ -151,6 +148,7 @@ fun ZoomableContentView(
content: BaseMediaContent, content: BaseMediaContent,
images: ImmutableList<BaseMediaContent> = remember(content) { listOf(content).toImmutableList() }, images: ImmutableList<BaseMediaContent> = remember(content) { listOf(content).toImmutableList() },
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
// store the dialog open or close state // store the dialog open or close state
@@ -189,37 +187,43 @@ fun ZoomableContentView(
when (content) { when (content) {
is MediaUrlImage -> is MediaUrlImage ->
SensitivityWarning(content.contentWarning != null, accountViewModel) { SensitivityWarning(content.contentWarning != null, accountViewModel) {
UrlImageView(content, mainImageModifier, accountViewModel = accountViewModel) UrlImageView(content, mainImageModifier, isFiniteHeight, accountViewModel = accountViewModel)
} }
is MediaUrlVideo -> is MediaUrlVideo ->
SensitivityWarning(content.contentWarning != null, accountViewModel) { SensitivityWarning(content.contentWarning != null, accountViewModel) {
VideoView( Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
videoUri = content.url, VideoView(
title = content.description, videoUri = content.url,
artworkUri = content.artworkUri, title = content.description,
authorName = content.authorName, artworkUri = content.artworkUri,
dimensions = content.dim, authorName = content.authorName,
blurhash = content.blurhash, dimensions = content.dim,
roundedCorner = roundedCorner, blurhash = content.blurhash,
nostrUriCallback = content.uri, roundedCorner = roundedCorner,
onDialog = { dialogOpen = true }, isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel, nostrUriCallback = content.uri,
) onDialog = { dialogOpen = true },
accountViewModel = accountViewModel,
)
}
} }
is MediaLocalImage -> is MediaLocalImage ->
LocalImageView(content, mainImageModifier, accountViewModel = accountViewModel) LocalImageView(content, mainImageModifier, isFiniteHeight, accountViewModel = accountViewModel)
is MediaLocalVideo -> is MediaLocalVideo ->
content.localFile?.let { content.localFile?.let {
VideoView( Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
videoUri = it.toUri().toString(), VideoView(
title = content.description, videoUri = it.toUri().toString(),
artworkUri = content.artworkUri, title = content.description,
authorName = content.authorName, artworkUri = content.artworkUri,
roundedCorner = roundedCorner, authorName = content.authorName,
nostrUriCallback = content.uri, roundedCorner = roundedCorner,
onDialog = { dialogOpen = true }, isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel, nostrUriCallback = content.uri,
) onDialog = { dialogOpen = true },
accountViewModel = accountViewModel,
)
}
} }
} }
@@ -232,12 +236,13 @@ fun ZoomableContentView(
private fun LocalImageView( private fun LocalImageView(
content: MediaLocalImage, content: MediaLocalImage,
mainImageModifier: Modifier, mainImageModifier: Modifier,
isFiniteHeight: Boolean,
topPaddingForControllers: Dp = Dp.Unspecified, topPaddingForControllers: Dp = Dp.Unspecified,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
alwayShowImage: Boolean = false, alwayShowImage: Boolean = false,
) { ) {
if (content.localFileExists()) { if (content.localFileExists()) {
BoxWithConstraints(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
val showImage = val showImage =
remember { remember {
mutableStateOf( 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 = val contentScale =
remember { remember {
if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth if (isFiniteHeight) ContentScale.Fit else ContentScale.FillWidth
} }
val verifierModifier = val ratio = remember(content) { aspectRatio(content.dim) }
if (topPaddingForControllers.isSpecified) {
Modifier.padding(top = topPaddingForControllers).align(Alignment.TopEnd)
} else {
Modifier.align(Alignment.TopEnd)
}
val painterState = remember { mutableStateOf<AsyncImagePainter.State?>(null) }
if (showImage.value) { if (showImage.value) {
AsyncImage( SubcomposeAsyncImage(
model = content.localFile, model = content.localFile,
contentDescription = content.description, contentDescription = content.description,
contentScale = contentScale, contentScale = contentScale,
modifier = myModifier, modifier = mainImageModifier,
onState = { painterState.value = it }, ) {
) 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( content.isVerified?.let {
painterState, val verifierModifier =
content, if (topPaddingForControllers.isSpecified) {
contentScale, Modifier.align(Alignment.TopEnd).padding(top = topPaddingForControllers)
myModifier, } else {
verifierModifier, Modifier.align(Alignment.TopEnd)
showImage, }
)
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 { } else {
BlankNote() BlankNote()
@@ -299,11 +339,12 @@ private fun LocalImageView(
private fun UrlImageView( private fun UrlImageView(
content: MediaUrlImage, content: MediaUrlImage,
mainImageModifier: Modifier, mainImageModifier: Modifier,
isFiniteHeight: Boolean,
topPaddingForControllers: Dp = Dp.Unspecified, topPaddingForControllers: Dp = Dp.Unspecified,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
alwayShowImage: Boolean = false, alwayShowImage: Boolean = false,
) { ) {
BoxWithConstraints(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
val showImage = val showImage =
remember { remember {
mutableStateOf( mutableStateOf(
@@ -311,42 +352,80 @@ private fun UrlImageView(
) )
} }
val myModifier = val contentScale = if (isFiniteHeight) ContentScale.Fit else ContentScale.FillWidth
remember {
mainImageModifier.widthIn(max = maxWidth).heightIn(max = maxHeight)
}
val contentScale = if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth val ratio = remember(content) { aspectRatio(content.dim) }
val verifierModifier =
if (topPaddingForControllers.isSpecified) {
Modifier.padding(top = topPaddingForControllers).align(Alignment.TopEnd)
} else {
Modifier.align(Alignment.TopEnd)
}
val painterState = remember { mutableStateOf<AsyncImagePainter.State?>(null) }
if (showImage.value) { if (showImage.value) {
AsyncImage( SubcomposeAsyncImage(
model = content.url, model = content.url,
contentDescription = content.description, contentDescription = content.description,
contentScale = contentScale, contentScale = contentScale,
modifier = myModifier, modifier = mainImageModifier,
onState = { ) {
painterState.value = it 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( val verifierModifier =
painterState, if (topPaddingForControllers.isSpecified) {
content, Modifier.align(Alignment.TopEnd).padding(top = topPaddingForControllers)
contentScale, } else {
myModifier, Modifier.align(Alignment.TopEnd)
verifierModifier, }
showImage,
) 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<Boolean>) =
} }
@Composable @Composable
@OptIn(ExperimentalLayoutApi::class) fun ShowHash(content: MediaUrlContent) {
private fun AddedImageFeatures(
painter: MutableState<AsyncImagePainter.State?>,
content: MediaLocalImage,
contentScale: ContentScale,
myModifier: Modifier,
verifiedModifier: Modifier,
showImage: MutableState<Boolean>,
) {
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<AsyncImagePainter.State?>,
content: MediaUrlImage,
contentScale: ContentScale,
myModifier: Modifier,
verifiedModifier: Modifier,
showImage: MutableState<Boolean>,
) {
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,
) {
var verifiedHash by remember(content.url) { mutableStateOf<Boolean?>(null) } var verifiedHash by remember(content.url) { mutableStateOf<Boolean?>(null) }
if (content.hash != 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? { fun aspectRatio(dim: String?): Float? {
@@ -777,6 +729,7 @@ private fun DialogContent(
RenderImageOrVideo( RenderImageOrVideo(
content = it, content = it,
roundedCorner = false, roundedCorner = false,
isFiniteHeight = true,
topPaddingForControllers = Size55dp, topPaddingForControllers = Size55dp,
onControllerVisibilityChanged = { controllerVisible.value = it }, onControllerVisibilityChanged = { controllerVisible.value = it },
onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value }, onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value },
@@ -788,6 +741,7 @@ private fun DialogContent(
RenderImageOrVideo( RenderImageOrVideo(
content = imageUrl, content = imageUrl,
roundedCorner = false, roundedCorner = false,
isFiniteHeight = true,
topPaddingForControllers = Size55dp, topPaddingForControllers = Size55dp,
onControllerVisibilityChanged = { controllerVisible.value = it }, onControllerVisibilityChanged = { controllerVisible.value = it },
onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value }, onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value },
@@ -801,7 +755,12 @@ private fun DialogContent(
exit = remember { fadeOut() }, exit = remember { fadeOut() },
) { ) {
Row( Row(
modifier = Modifier.padding(10.dp).statusBarsPadding().systemBarsPadding().fillMaxWidth(), modifier =
Modifier
.padding(10.dp)
.statusBarsPadding()
.systemBarsPadding()
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@@ -928,6 +887,7 @@ private fun ShareImageAction(
private fun RenderImageOrVideo( private fun RenderImageOrVideo(
content: BaseMediaContent, content: BaseMediaContent,
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean,
topPaddingForControllers: Dp = Dp.Unspecified, topPaddingForControllers: Dp = Dp.Unspecified,
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
onToggleControllerVisibility: (() -> Unit)? = null, onToggleControllerVisibility: (() -> Unit)? = null,
@@ -937,7 +897,8 @@ private fun RenderImageOrVideo(
if (content is MediaUrlImage) { if (content is MediaUrlImage) {
val mainModifier = val mainModifier =
Modifier.fillMaxSize() Modifier
.fillMaxSize()
.zoomable( .zoomable(
rememberZoomState(), rememberZoomState(),
onTap = { onTap = {
@@ -950,18 +911,20 @@ private fun RenderImageOrVideo(
UrlImageView( UrlImageView(
content = content, content = content,
mainImageModifier = mainModifier, mainImageModifier = mainModifier,
isFiniteHeight = isFiniteHeight,
topPaddingForControllers = topPaddingForControllers, topPaddingForControllers = topPaddingForControllers,
accountViewModel, accountViewModel = accountViewModel,
alwayShowImage = true, alwayShowImage = true,
) )
} else if (content is MediaUrlVideo) { } 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( VideoViewInner(
videoUri = content.url, videoUri = content.url,
title = content.description, title = content.description,
artworkUri = content.artworkUri, artworkUri = content.artworkUri,
authorName = content.authorName, authorName = content.authorName,
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
topPaddingForControllers = topPaddingForControllers, topPaddingForControllers = topPaddingForControllers,
automaticallyStartPlayback = automaticallyStartPlayback, automaticallyStartPlayback = automaticallyStartPlayback,
onControllerVisibilityChanged = onControllerVisibilityChanged, onControllerVisibilityChanged = onControllerVisibilityChanged,
@@ -969,7 +932,8 @@ private fun RenderImageOrVideo(
} }
} else if (content is MediaLocalImage) { } else if (content is MediaLocalImage) {
val mainModifier = val mainModifier =
Modifier.fillMaxSize() Modifier
.fillMaxSize()
.zoomable( .zoomable(
rememberZoomState(), rememberZoomState(),
onTap = { onTap = {
@@ -982,12 +946,13 @@ private fun RenderImageOrVideo(
LocalImageView( LocalImageView(
content = content, content = content,
mainImageModifier = mainModifier, mainImageModifier = mainModifier,
isFiniteHeight = isFiniteHeight,
topPaddingForControllers = topPaddingForControllers, topPaddingForControllers = topPaddingForControllers,
accountViewModel, accountViewModel = accountViewModel,
alwayShowImage = true, alwayShowImage = true,
) )
} else if (content is MediaLocalVideo) { } 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 { content.localFile?.let {
VideoViewInner( VideoViewInner(
videoUri = it.toUri().toString(), videoUri = it.toUri().toString(),
@@ -995,6 +960,7 @@ private fun RenderImageOrVideo(
artworkUri = content.artworkUri, artworkUri = content.artworkUri,
authorName = content.authorName, authorName = content.authorName,
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
topPaddingForControllers = topPaddingForControllers, topPaddingForControllers = topPaddingForControllers,
automaticallyStartPlayback = automaticallyStartPlayback, automaticallyStartPlayback = automaticallyStartPlayback,
onControllerVisibilityChanged = onControllerVisibilityChanged, onControllerVisibilityChanged = onControllerVisibilityChanged,
@@ -1020,12 +986,8 @@ private suspend fun verifyHash(content: MediaUrlContent): Boolean? {
} }
@Composable @Composable
private fun HashVerificationSymbol( private fun HashVerificationSymbol(verifiedHash: Boolean) {
verifiedHash: Boolean,
modifier: Modifier,
) {
val localContext = LocalContext.current val localContext = LocalContext.current
val openDialogMsg = remember { mutableStateOf<String?>(null) } val openDialogMsg = remember { mutableStateOf<String?>(null) }
openDialogMsg.value?.let { openDialogMsg.value?.let {
@@ -1037,25 +999,23 @@ private fun HashVerificationSymbol(
} }
} }
Box( if (verifiedHash) {
modifier.width(40.dp).height(40.dp).padding(10.dp), IconButton(
) { modifier = hashVerifierMark,
if (verifiedHash) { onClick = {
IconButton( openDialogMsg.value = localContext.getString(R.string.hash_verification_passed)
onClick = { },
openDialogMsg.value = localContext.getString(R.string.hash_verification_passed) ) {
}, HashCheckIcon(Size30dp)
) { }
HashCheckIcon(Size30dp) } else {
} IconButton(
} else { modifier = hashVerifierMark,
IconButton( onClick = {
onClick = { openDialogMsg.value = localContext.getString(R.string.hash_verification_failed)
openDialogMsg.value = localContext.getString(R.string.hash_verification_failed) },
}, ) {
) { HashCheckFailedIcon(Size30dp)
HashCheckFailedIcon(Size30dp)
}
} }
} }
} }
@@ -95,6 +95,7 @@ class MarkdownMediaRenderer(
ZoomableContentView( ZoomableContentView(
content = content, content = content,
roundedCorner = true, roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} }
@@ -116,6 +117,7 @@ class MarkdownMediaRenderer(
ZoomableContentView( ZoomableContentView(
content = content, content = content,
roundedCorner = true, roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} }
@@ -29,10 +29,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth 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.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.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -40,32 +40,28 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.compositeOver 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.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.R
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji 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.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.HalfTopPadding
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing 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.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer 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.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.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.messageBubbleLimits
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
@@ -132,7 +130,6 @@ fun ChatroomMessageCompose(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun NormalChatNote( fun NormalChatNote(
note: Note, note: Note,
@@ -145,6 +142,17 @@ fun NormalChatNote(
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
onWantsToEditDraft: (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 val drawAuthorInfo by
remember(note) { remember(note) {
derivedStateOf { 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<Color>? = null,
onClick: () -> Boolean,
onAuthorClick: () -> Unit,
actionMenu: @Composable (popupExpanded: Boolean, onDismiss: () -> Unit) -> Unit,
detailRow: @Composable () -> Unit,
drawAuthorLine: @Composable () -> Unit,
inner: @Composable (MutableState<Color>) -> Unit,
) {
val loggedInColors = MaterialTheme.colorScheme.mediumImportanceLink val loggedInColors = MaterialTheme.colorScheme.mediumImportanceLink
val otherColors = MaterialTheme.colorScheme.subtleBorder val otherColors = MaterialTheme.colorScheme.subtleBorder
val defaultBackground = MaterialTheme.colorScheme.background val defaultBackground = MaterialTheme.colorScheme.background
val backgroundBubbleColor = val backgroundBubbleColor =
remember { remember {
if (accountViewModel.isLoggedUser(note.author)) { if (isLoggedInUser) {
mutableStateOf( mutableStateOf(
loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground), loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground),
) )
@@ -176,182 +275,176 @@ fun NormalChatNote(
mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground)) mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground))
} }
} }
val alignment: Arrangement.Horizontal = val alignment: Arrangement.Horizontal =
remember { if (isLoggedInUser) {
if (accountViewModel.isLoggedUser(note.author)) { Arrangement.End
Arrangement.End } else {
} else { Arrangement.Start
Arrangement.Start
}
} }
val shape: Shape = val shape: Shape =
remember { if (isLoggedInUser) {
if (accountViewModel.isLoggedUser(note.author)) { ChatBubbleShapeMe
ChatBubbleShapeMe } else {
} else { ChatBubbleShapeThem
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) { val clickableModifier =
LaunchedEffect(key1 = routeForLastRead) { remember {
accountViewModel.loadAndMarkAsRead(routeForLastRead, note.createdAt()) Modifier.combinedClickable(
} onClick = {
} if (!onClick()) {
if (isSimplified) {
Column { showDetails.value = !showDetails.value
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
}
} }
},
onLongClick = { popupExpanded = true },
)
}
Row(
horizontalArrangement = alignment,
modifier =
modif2.onSizeChanged {
if (availableBubbleSize.intValue != it.width) {
availableBubbleSize.intValue = it.width
} }
}, },
) { onLongClick = { popupExpanded = true },
Surface( )
color = backgroundBubbleColor.value,
shape = shape,
modifier = clickableModifier,
) {
RenderBubble(
note,
drawAuthorInfo,
alignment,
innerQuote,
backgroundBubbleColor,
onWantsToReply,
onWantsToEditDraft,
canPreview,
availableBubbleSize,
showDetails,
accountViewModel,
nav,
)
}
} }
NoteQuickActionMenu( Row(
note = note, horizontalArrangement = alignment,
popupExpanded = popupExpanded, modifier = modif2,
onDismiss = { popupExpanded = false }, ) {
onWantsToEditDraft = { onWantsToEditDraft(note) }, Surface(
accountViewModel = accountViewModel, color = backgroundBubbleColor.value,
nav = nav, 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 @Composable
private fun RenderBubble( private fun BubblePreview() {
baseNote: Note, val backgroundBubbleColor =
drawAuthorInfo: Boolean,
alignment: Arrangement.Horizontal,
innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>,
onWantsToReply: (Note) -> Unit,
onWantsToEditDraft: (Note) -> Unit,
canPreview: Boolean,
availableBubbleSize: MutableState<Int>,
showDetails: State<Boolean>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val bubbleSize = remember { mutableIntStateOf(0) }
val bubbleModifier =
remember { remember {
Modifier mutableStateOf<Color>(Color.Transparent)
.padding(start = 10.dp, end = 10.dp, bottom = 5.dp)
.onSizeChanged {
if (bubbleSize.intValue != it.width) {
bubbleSize.intValue = it.width
}
}
} }
Column(modifier = bubbleModifier) { Column {
MessageBubbleLines( ChatBubbleLayout(
drawAuthorInfo, isLoggedInUser = false,
baseNote, innerQuote = false,
alignment, isSimplified = false,
availableBubbleSize, hasDetailsToShow = true,
innerQuote, drawAuthorInfo = true,
backgroundBubbleColor, parentBackgroundColor = backgroundBubbleColor,
bubbleSize, onClick = { false },
onWantsToReply, onAuthorClick = {},
onWantsToEditDraft, actionMenu = { popupExpanded, onDismiss ->
canPreview, },
showDetails, drawAuthorLine = {
accountViewModel, UserDisplayNameLayout(
nav, 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 @Composable
private fun MessageBubbleLines( private fun MessageBubbleLines(
drawAuthorInfo: Boolean,
baseNote: Note, baseNote: Note,
alignment: Arrangement.Horizontal,
availableBubbleSize: MutableState<Int>,
innerQuote: Boolean, innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>, backgroundBubbleColor: MutableState<Color>,
bubbleSize: MutableState<Int>,
onWantsToReply: (Note) -> Unit, onWantsToReply: (Note) -> Unit,
onWantsToEditDraft: (Note) -> Unit, onWantsToEditDraft: (Note) -> Unit,
canPreview: Boolean, canPreview: Boolean,
showDetails: State<Boolean>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
if (drawAuthorInfo) {
DrawAuthorInfo(
baseNote,
alignment,
accountViewModel.settings.showProfilePictures.value,
accountViewModel,
nav,
)
}
if (baseNote.event !is DraftEvent) { if (baseNote.event !is DraftEvent) {
RenderReplyRow( RenderReplyRow(
note = baseNote, note = baseNote,
@@ -374,41 +467,6 @@ private fun MessageBubbleLines(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, 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 @Composable
@@ -536,43 +594,9 @@ private fun RenderDraftEvent(
@Composable @Composable
private fun ConstrainedStatusRow( private fun ConstrainedStatusRow(
bubbleSize: MutableState<Int>,
availableBubbleSize: MutableState<Int>,
firstColumn: @Composable () -> Unit, firstColumn: @Composable () -> Unit,
secondColumn: @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 @Composable
@@ -703,68 +727,56 @@ private fun RenderCreateChannelNote(note: Note) {
@Composable @Composable
private fun DrawAuthorInfo( private fun DrawAuthorInfo(
baseNote: Note, baseNote: Note,
alignment: Arrangement.Horizontal,
loadProfilePicture: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
baseNote.author?.let { baseNote.author?.let {
Row( WatchAndDisplayUser(it, accountViewModel, nav)
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = alignment,
modifier =
Modifier
.padding(top = Size10dp)
.clickable {
nav("User/${baseNote.author?.pubkeyHex}")
},
) {
WatchAndDisplayUser(it, loadProfilePicture, accountViewModel, nav)
}
} }
} }
@Composable
fun UserDisplayNameLayout(
picture: @Composable () -> Unit,
name: @Composable () -> Unit,
) {
Box(chatAuthorBox, contentAlignment = Alignment.TopEnd) {
picture()
}
Spacer(modifier = StdHorzSpacer)
name()
}
@Composable @Composable
private fun WatchAndDisplayUser( private fun WatchAndDisplayUser(
author: User, author: User,
loadProfilePicture: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val userState by author.live().userMetadataInfo.observeAsState() val userState by author.live().userMetadataInfo.observeAsState()
Box(chatAuthorBox, contentAlignment = Alignment.TopEnd) { UserDisplayNameLayout(
InnerUserPicture( picture = {
userHex = author.pubkeyHex, InnerUserPicture(
userPicture = userState?.picture, userHex = author.pubkeyHex,
userName = userState?.bestName(), userPicture = userState?.picture,
size = Size20dp, userName = userState?.bestName(),
modifier = Modifier, size = Size20dp,
accountViewModel = accountViewModel, modifier = Modifier,
) accountViewModel = accountViewModel,
)
ObserveAndDisplayFollowingMark(author.pubkeyHex, Size5dp, accountViewModel) ObserveAndDisplayFollowingMark(author.pubkeyHex, Size5dp, accountViewModel)
} },
name = {
if (userState != null) { if (userState != null) {
DisplayMessageUsername(userState?.bestName() ?: author.pubkeyDisplayHex(), userState?.tags ?: EmptyTagList) DisplayMessageUsername(userState?.bestName() ?: author.pubkeyDisplayHex(), userState?.tags ?: EmptyTagList)
} else { } else {
DisplayMessageUsername(author.pubkeyDisplayHex(), EmptyTagList) 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,
) )
} }
@@ -773,7 +785,6 @@ private fun DisplayMessageUsername(
userDisplayName: String, userDisplayName: String,
userTags: ImmutableListOfLists<String>, userTags: ImmutableListOfLists<String>,
) { ) {
Spacer(modifier = StdHorzSpacer)
CreateTextWithEmoji( CreateTextWithEmoji(
text = userDisplayName, text = userDisplayName,
tags = userTags, tags = userTags,
@@ -303,8 +303,8 @@ fun AcceptableNote(
) )
} }
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote) is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel) is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, false, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, false, accountViewModel)
else -> else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote( CheckNewAndRenderNote(
@@ -587,8 +587,8 @@ private fun RenderNoteRow(
val noteEvent = baseNote.event val noteEvent = baseNote.event
when (noteEvent) { when (noteEvent) {
is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav) is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav)
is AudioTrackEvent -> RenderAudioTrack(baseNote, accountViewModel, nav) is AudioTrackEvent -> RenderAudioTrack(baseNote, false, accountViewModel, nav)
is AudioHeaderEvent -> RenderAudioHeader(baseNote, accountViewModel, nav) is AudioHeaderEvent -> RenderAudioHeader(baseNote, false, accountViewModel, nav)
is DraftEvent -> RenderDraft(baseNote, quotesLeft, unPackReply, backgroundColor, accountViewModel, nav) is DraftEvent -> RenderDraft(baseNote, quotesLeft, unPackReply, backgroundColor, accountViewModel, nav)
is ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
@@ -706,10 +706,10 @@ private fun RenderNoteRow(
nav, nav,
) )
} }
is FileHeaderEvent -> FileHeaderDisplay(baseNote, true, accountViewModel) is FileHeaderEvent -> FileHeaderDisplay(baseNote, true, false, accountViewModel)
is VideoHorizontalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav) is VideoHorizontalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, false, accountViewModel, nav)
is VideoVerticalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav) is VideoVerticalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, false, accountViewModel, nav)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, accountViewModel) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, false, accountViewModel)
is CommunityPostApprovalEvent -> { is CommunityPostApprovalEvent -> {
RenderPostApproval( RenderPostApproval(
baseNote, baseNote,
@@ -60,18 +60,20 @@ import java.util.Locale
@Composable @Composable
fun RenderAudioTrack( fun RenderAudioTrack(
note: Note, note: Note,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val noteEvent = note.event as? AudioTrackEvent ?: return val noteEvent = note.event as? AudioTrackEvent ?: return
AudioTrackHeader(noteEvent, note, accountViewModel, nav) AudioTrackHeader(noteEvent, note, isFiniteHeight, accountViewModel, nav)
} }
@Composable @Composable
fun AudioTrackHeader( fun AudioTrackHeader(
noteEvent: AudioTrackEvent, noteEvent: AudioTrackEvent,
note: Note, note: Note,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
@@ -144,17 +146,20 @@ fun AudioTrackHeader(
thumbUri = cover, thumbUri = cover,
authorName = note.author?.toBestDisplayName(), authorName = note.author?.toBestDisplayName(),
roundedCorner = true, roundedCorner = true,
isFiniteHeight = isFiniteHeight,
nostrUriCallback = "nostr:${note.toNEvent()}", nostrUriCallback = "nostr:${note.toNEvent()}",
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} } ?: run {
?: VideoView( VideoView(
videoUri = media, videoUri = media,
title = noteEvent.subject(), title = noteEvent.subject(),
authorName = note.author?.toBestDisplayName(), authorName = note.author?.toBestDisplayName(),
roundedCorner = true, roundedCorner = true,
isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
}
} }
} }
} }
@@ -164,18 +169,20 @@ fun AudioTrackHeader(
@Composable @Composable
fun RenderAudioHeader( fun RenderAudioHeader(
note: Note, note: Note,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val noteEvent = note.event as? AudioHeaderEvent ?: return val noteEvent = note.event as? AudioHeaderEvent ?: return
AudioHeader(noteEvent, note, accountViewModel, nav) AudioHeader(noteEvent, note, isFiniteHeight, accountViewModel, nav)
} }
@Composable @Composable
fun AudioHeader( fun AudioHeader(
noteEvent: AudioHeaderEvent, noteEvent: AudioHeaderEvent,
note: Note, note: Note,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
@@ -199,6 +206,7 @@ fun AudioHeader(
title = noteEvent.subject(), title = noteEvent.subject(),
authorName = note.author?.toBestDisplayName(), authorName = note.author?.toBestDisplayName(),
roundedCorner = true, roundedCorner = true,
isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nostrUriCallback = note.toNostrUri(), nostrUriCallback = note.toNostrUri(),
) )
@@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.events.FileHeaderEvent
fun FileHeaderDisplay( fun FileHeaderDisplay(
note: Note, note: Note,
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val event = (note.event as? FileHeaderEvent) ?: return val event = (note.event as? FileHeaderEvent) ?: return
@@ -80,6 +81,7 @@ fun FileHeaderDisplay(
ZoomableContentView( ZoomableContentView(
content = content, content = content,
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} }
@@ -42,6 +42,7 @@ import java.io.File
fun FileStorageHeaderDisplay( fun FileStorageHeaderDisplay(
baseNote: Note, baseNote: Note,
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
@@ -49,7 +50,7 @@ fun FileStorageHeaderDisplay(
LoadNote(baseNoteHex = dataEventId, accountViewModel) { contentNote -> LoadNote(baseNoteHex = dataEventId, accountViewModel) { contentNote ->
if (contentNote != null) { if (contentNote != null) {
ObserverAndRenderNIP95(baseNote, contentNote, roundedCorner, accountViewModel) ObserverAndRenderNIP95(baseNote, contentNote, roundedCorner, isFiniteHeight, accountViewModel)
} }
} }
} }
@@ -59,6 +60,7 @@ private fun ObserverAndRenderNIP95(
header: Note, header: Note,
content: Note, content: Note,
roundedCorner: Boolean, roundedCorner: Boolean,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return
@@ -110,6 +112,7 @@ private fun ObserverAndRenderNIP95(
ZoomableContentView( ZoomableContentView(
content = it, content = it,
roundedCorner = roundedCorner, roundedCorner = roundedCorner,
isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} }
@@ -156,6 +156,7 @@ fun RenderLiveActivityEventInner(
artworkUri = cover, artworkUri = cover,
authorName = baseNote.author?.toBestDisplayName(), authorName = baseNote.author?.toBestDisplayName(),
roundedCorner = true, roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nostrUriCallback = "nostr:${baseNote.toNEvent()}", nostrUriCallback = "nostr:${baseNote.toNEvent()}",
) )
@@ -66,6 +66,7 @@ fun VideoDisplay(
makeItShort: Boolean, makeItShort: Boolean,
canPreview: Boolean, canPreview: Boolean,
backgroundColor: MutableState<Color>, backgroundColor: MutableState<Color>,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
@@ -143,6 +144,7 @@ fun VideoDisplay(
ZoomableContentView( ZoomableContentView(
content = content, content = content,
roundedCorner = true, roundedCorner = true,
isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} }
@@ -504,17 +504,17 @@ fun NoteMaster(
nav = nav, nav = nav,
) )
} else if (noteEvent is VideoEvent) { } 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) { } else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote, true, accountViewModel) FileHeaderDisplay(baseNote, true, false, accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) { } else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote, true, accountViewModel) FileStorageHeaderDisplay(baseNote, true, false, accountViewModel)
} else if (noteEvent is PeopleListEvent) { } else if (noteEvent is PeopleListEvent) {
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav) DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
} else if (noteEvent is AudioTrackEvent) { } else if (noteEvent is AudioTrackEvent) {
AudioTrackHeader(noteEvent, baseNote, accountViewModel, nav) AudioTrackHeader(noteEvent, baseNote, false, accountViewModel, nav)
} else if (noteEvent is AudioHeaderEvent) { } else if (noteEvent is AudioHeaderEvent) {
AudioHeader(noteEvent, baseNote, accountViewModel, nav) AudioHeader(noteEvent, baseNote, false, accountViewModel, nav)
} else if (noteEvent is CommunityPostApprovalEvent) { } else if (noteEvent is CommunityPostApprovalEvent) {
RenderPostApproval( RenderPostApproval(
baseNote, baseNote,
@@ -743,6 +743,7 @@ fun ShowVideoStreaming(
ZoomableContentView( ZoomableContentView(
content = zoomableUrlVideo, content = zoomableUrlVideo,
roundedCorner = false, roundedCorner = false,
isFiniteHeight = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) )
} }
@@ -86,7 +86,7 @@ fun ChatroomListScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val windowSizeClass = accountViewModel.settings.windowSizeClass.value val windowSizeClass by accountViewModel.settings.windowSizeClass
val twoPane by remember { val twoPane by remember {
derivedStateOf { derivedStateOf {
@@ -104,7 +104,7 @@ fun ChatroomListScreen(
ChatroomListTwoPane( ChatroomListTwoPane(
knownFeedViewModel = knownFeedViewModel, knownFeedViewModel = knownFeedViewModel,
newFeedViewModel = newFeedViewModel, newFeedViewModel = newFeedViewModel,
widthSizeClass = windowSizeClass.widthSizeClass, widthSizeClass = windowSizeClass!!.widthSizeClass,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
@@ -325,12 +325,12 @@ private fun RenderVideoOrPictureNote(
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
Column(Modifier.fillMaxSize(1f), verticalArrangement = Arrangement.Center) { 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 } val noteEvent = remember { note.event }
if (noteEvent is FileHeaderEvent) { if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(note, false, accountViewModel) FileHeaderDisplay(note, false, true, accountViewModel)
} else if (noteEvent is FileStorageHeaderEvent) { } else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(note, false, accountViewModel) FileStorageHeaderDisplay(note, false, true, accountViewModel)
} }
} }
} }
@@ -108,6 +108,7 @@ val StdPadding = Modifier.padding(10.dp)
val BigPadding = Modifier.padding(15.dp) val BigPadding = Modifier.padding(15.dp)
val RowColSpacing = Arrangement.spacedBy(3.dp) val RowColSpacing = Arrangement.spacedBy(3.dp)
val RowColSpacing5dp = Arrangement.spacedBy(5.dp)
val HalfHorzPadding = Modifier.padding(horizontal = 5.dp) val HalfHorzPadding = Modifier.padding(horizontal = 5.dp)
val HalfVertPadding = Modifier.padding(vertical = 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 chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape)
val AuthorInfoVideoFeed = Modifier.width(75.dp).padding(end = 15.dp) 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 = val inlinePlaceholder =
Placeholder( Placeholder(
width = Font17SP, width = Font17SP,
@@ -247,3 +251,5 @@ val incognitoIconModifier =
Modifier Modifier
.padding(top = 1.dp) .padding(top = 1.dp)
.size(14.dp) .size(14.dp)
val hashVerifierMark = Modifier.width(40.dp).height(40.dp).padding(10.dp)