- 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:
@@ -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() },
|
||||
) {
|
||||
|
||||
+215
-255
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user