- 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)
}
LaunchedEffect(key1 = displayFeatures, key2 = windowSizeClass) {
sharedPreferencesViewModel.updateDisplaySettings(windowSizeClass, displayFeatures)
}
LaunchedEffect(act.isOnMobileDataState) {
sharedPreferencesViewModel.updateConnectivityStatusState(act.isOnMobileDataState)
}
@@ -305,6 +305,7 @@ fun EditPostView(
VideoView(
myUrlPreview,
roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel,
)
} else {
@@ -252,6 +252,7 @@ fun ImageVideoPost(
VideoView(
videoUri = it.toString(),
roundedCorner = false,
isFiniteHeight = false,
accountViewModel = accountViewModel,
)
}
@@ -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)
}
}
@@ -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,
)
}
@@ -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)
}
}
}
@@ -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<Int>? = 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<Int>? = 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>(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<Boolean>,
startingMuteState: Boolean,
topPadding: State<Dp>,
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<Boolean>,
controllerVisible: MutableState<Boolean>,
topPadding: State<Dp>,
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() },
) {
@@ -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<BaseMediaContent> = 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<AsyncImagePainter.State?>(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<AsyncImagePainter.State?>(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<Boolean>) =
}
@Composable
@OptIn(ExperimentalLayoutApi::class)
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,
) {
fun ShowHash(content: MediaUrlContent) {
var verifiedHash by remember(content.url) { mutableStateOf<Boolean?>(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<String?>(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)
}
}
}
@@ -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,
)
}
@@ -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<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 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<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 =
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>(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<Int>,
innerQuote: Boolean,
backgroundBubbleColor: MutableState<Color>,
bubbleSize: MutableState<Int>,
onWantsToReply: (Note) -> Unit,
onWantsToEditDraft: (Note) -> Unit,
canPreview: Boolean,
showDetails: State<Boolean>,
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<Int>,
availableBubbleSize: MutableState<Int>,
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<String>,
) {
Spacer(modifier = StdHorzSpacer)
CreateTextWithEmoji(
text = userDisplayName,
tags = userTags,
@@ -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,
@@ -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(),
)
@@ -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,
)
}
@@ -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,
)
}
@@ -156,6 +156,7 @@ fun RenderLiveActivityEventInner(
artworkUri = cover,
authorName = baseNote.author?.toBestDisplayName(),
roundedCorner = true,
isFiniteHeight = false,
accountViewModel = accountViewModel,
nostrUriCallback = "nostr:${baseNote.toNEvent()}",
)
@@ -66,6 +66,7 @@ fun VideoDisplay(
makeItShort: Boolean,
canPreview: Boolean,
backgroundColor: MutableState<Color>,
isFiniteHeight: Boolean,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
@@ -143,6 +144,7 @@ fun VideoDisplay(
ZoomableContentView(
content = content,
roundedCorner = true,
isFiniteHeight = isFiniteHeight,
accountViewModel = accountViewModel,
)
}
@@ -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,
@@ -743,6 +743,7 @@ fun ShowVideoStreaming(
ZoomableContentView(
content = zoomableUrlVideo,
roundedCorner = false,
isFiniteHeight = false,
accountViewModel = accountViewModel,
)
}
@@ -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,
)
@@ -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)
}
}
}
@@ -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)