Simplify RenderRegularWithGallery

Simplify collectConsecutiveImageParagraphs: just return the end index
Extracting common signature parameters into a RenderContext data class
Extracting GalleryImage helper function
This commit is contained in:
davotoula
2025-09-11 20:58:15 +02:00
parent 062182a7ec
commit 1f6d7d3fd2
2 changed files with 167 additions and 165 deletions

View File

@@ -42,6 +42,26 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import kotlinx.collections.immutable.ImmutableList
@Composable
private fun GalleryImage(
image: MediaUrlImage,
allImages: ImmutableList<MediaUrlImage>,
modifier: Modifier,
roundedCorner: Boolean,
contentScale: ContentScale,
accountViewModel: AccountViewModel,
) {
Box(modifier = modifier) {
ZoomableContentView(
content = image,
images = allImages,
roundedCorner = roundedCorner,
contentScale = contentScale,
accountViewModel = accountViewModel,
)
}
}
@Composable
fun ImageGallery(
images: ImmutableList<MediaUrlImage>,
@@ -55,15 +75,14 @@ fun ImageGallery(
}
images.size == 1 -> {
// Single image - display full width
Box(modifier = modifier.fillMaxWidth()) {
ZoomableContentView(
content = images.first(),
images = images,
roundedCorner = roundedCorner,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
}
GalleryImage(
image = images.first(),
allImages = images,
modifier = modifier.fillMaxWidth(),
roundedCorner = roundedCorner,
contentScale = ContentScale.FillWidth,
accountViewModel = accountViewModel,
)
}
images.size == 2 -> {
// Two images - side by side in 4:3 ratio
@@ -116,15 +135,14 @@ private fun TwoImageGallery(
horizontalArrangement = Arrangement.spacedBy(Size5dp),
) {
repeat(2) { index ->
Box(modifier = Modifier.weight(1f).fillMaxSize()) {
ZoomableContentView(
content = images[index],
images = images,
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
GalleryImage(
image = images[index],
allImages = images,
modifier = Modifier.weight(1f).fillMaxSize(),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
}
@@ -141,15 +159,14 @@ private fun ThreeImageGallery(
horizontalArrangement = Arrangement.spacedBy(Size5dp),
) {
// Large image on the left
Box(modifier = Modifier.weight(2f).fillMaxSize()) {
ZoomableContentView(
content = images[0],
images = images,
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
GalleryImage(
image = images[0],
allImages = images,
modifier = Modifier.weight(2f).fillMaxSize(),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
// Two smaller images on the right
Column(
@@ -157,15 +174,14 @@ private fun ThreeImageGallery(
verticalArrangement = Arrangement.spacedBy(Size5dp),
) {
repeat(2) { index ->
Box(modifier = Modifier.weight(1f).fillMaxSize()) {
ZoomableContentView(
content = images[index + 1],
images = images,
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
GalleryImage(
image = images[index + 1],
allImages = images,
modifier = Modifier.weight(1f).fillMaxSize(),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
}
@@ -189,15 +205,14 @@ private fun FourImageGallery(
) {
repeat(2) { colIndex ->
val imageIndex = rowIndex * 2 + colIndex
Box(modifier = Modifier.weight(1f).fillMaxSize()) {
ZoomableContentView(
content = images[imageIndex],
images = images,
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
GalleryImage(
image = images[imageIndex],
allImages = images,
modifier = Modifier.weight(1f).fillMaxSize(),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
}
@@ -222,15 +237,14 @@ private fun ManyImageGallery(
modifier = Modifier.padding(Size5dp),
) {
items(images) { image ->
Box(modifier = Modifier.aspectRatio(1f)) {
ZoomableContentView(
content = image,
images = images,
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
GalleryImage(
image = image,
allImages = images,
modifier = Modifier.aspectRatio(1f),
roundedCorner = roundedCorner,
contentScale = ContentScale.Crop,
accountViewModel = accountViewModel,
)
}
}
}

View File

@@ -114,6 +114,15 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
data class RenderContext(
val state: RichTextViewerState,
val backgroundColor: MutableState<Color>,
val quotesLeft: Int,
val callbackUri: String?,
val accountViewModel: AccountViewModel,
val nav: INav,
)
fun isMarkdown(content: String): Boolean =
content.startsWith("> ") ||
content.startsWith("# ") ||
@@ -313,84 +322,78 @@ fun RenderRegularWithGallery(
) {
val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags, callbackUri)) }
val context =
RenderContext(
state = state,
backgroundColor = backgroundColor,
quotesLeft = quotesLeft,
callbackUri = callbackUri,
accountViewModel = accountViewModel,
nav = nav,
)
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
Column {
// Process paragraphs and group consecutive image-only paragraphs
// Process each paragraph uniformly
var i = 0
while (i < state.paragraphs.size) {
val paragraph = state.paragraphs[i]
// Check if this paragraph contains only images
val isImageOnlyParagraph =
paragraph.words.all { word ->
word is ImageSegment || word is Base64Segment
}
if (isImageOnlyParagraph && paragraph.words.isNotEmpty()) {
// Collect consecutive image-only paragraphs
val (imageParagraphs, totalProcessedCount) = collectConsecutiveImageParagraphs(state.paragraphs, i)
// Combine all image words from consecutive paragraphs
val allImageWords = imageParagraphs.flatMap { it.words }.toImmutableList()
if (allImageWords.size > 1) {
// Multiple images - render as gallery (no FlowRow wrapper needed)
RenderWordsWithImageGallery(
allImageWords,
state,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
)
} else {
// Single image - render normally
RenderParagraphWithFlowRow(
paragraph,
paragraph.words.toImmutableList(),
spaceWidth,
state,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
)
}
i += totalProcessedCount // Skip processed paragraphs (including empty ones)
} else {
// Non-image paragraph - render normally
RenderParagraphWithFlowRow(
paragraph,
paragraph.words.toImmutableList(),
spaceWidth,
state,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
i =
renderParagraphWithFlowRow(
paragraphs = state.paragraphs,
paragraphIndex = i,
spaceWidth = spaceWidth,
context = context,
)
i++
}
}
}
}
@Composable
private fun RenderParagraphWithFlowRow(
private fun renderParagraphWithFlowRow(
paragraphs: ImmutableList<ParagraphState>,
paragraphIndex: Int,
spaceWidth: Dp,
context: RenderContext,
): Int {
val paragraph = paragraphs[paragraphIndex]
// Check if this paragraph contains only images
val isImageOnlyParagraph =
paragraph.words.all { word ->
word is ImageSegment || word is Base64Segment
}
if (isImageOnlyParagraph && paragraph.words.isNotEmpty()) {
// Collect consecutive image-only paragraphs for gallery
val (imageParagraphs, endIndex) = collectConsecutiveImageParagraphs(paragraphs, paragraphIndex)
val allImageWords = imageParagraphs.flatMap { it.words }.toImmutableList()
if (allImageWords.size > 1) {
// Multiple images - render as gallery (no FlowRow wrapper needed)
RenderWordsWithImageGallery(
allImageWords,
context,
)
} else {
// Single image - render with FlowRow wrapper
RenderSingleParagraphWithFlowRow(paragraph, paragraph.words.toImmutableList(), spaceWidth, context)
}
return endIndex // Return next index to process
} else {
// Non-image paragraph - render normally with FlowRow
RenderSingleParagraphWithFlowRow(paragraph, paragraph.words.toImmutableList(), spaceWidth, context)
return paragraphIndex + 1 // Return next index to process
}
}
@Composable
private fun RenderSingleParagraphWithFlowRow(
paragraph: ParagraphState,
words: ImmutableList<Segment>,
spaceWidth: Dp,
state: RichTextViewerState,
backgroundColor: MutableState<Color>,
quotesLeft: Int,
callbackUri: String?,
accountViewModel: AccountViewModel,
nav: INav,
context: RenderContext,
) {
CompositionLocalProvider(
LocalLayoutDirection provides
@@ -406,12 +409,7 @@ private fun RenderParagraphWithFlowRow(
) {
RenderWordsWithImageGallery(
words,
state,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
context,
)
}
}
@@ -453,7 +451,7 @@ private fun collectConsecutiveImageParagraphs(
break
}
}
return Pair(imageParagraphs, j - startIndex) // Return paragraphs and total processed count
return Pair(imageParagraphs, j) // Return collected paragraphs and next index to process
}
@OptIn(ExperimentalLayoutApi::class)
@@ -551,12 +549,7 @@ private fun RenderWordWithoutPreview(
@Composable
private fun RenderWordsWithImageGallery(
words: ImmutableList<Segment>,
state: RichTextViewerState,
backgroundColor: MutableState<Color>,
quotesLeft: Int,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: INav,
context: RenderContext,
) {
var i = 0
while (i < words.size) {
@@ -577,25 +570,25 @@ private fun RenderWordsWithImageGallery(
imageSegments
.mapNotNull { segment ->
val imageUrl = segment.segmentText
state.imagesForPager[imageUrl] as? MediaUrlImage
context.state.imagesForPager[imageUrl] as? MediaUrlImage
}.toImmutableList()
if (imageContents.isNotEmpty()) {
ImageGallery(
images = imageContents,
accountViewModel = accountViewModel,
accountViewModel = context.accountViewModel,
roundedCorner = true,
)
}
} else {
// Single image - render normally
RenderWordWithPreview(word, state, backgroundColor, quotesLeft, callbackUri, accountViewModel, nav)
RenderWordWithPreview(word, context)
}
i = j // Skip processed images
} else {
// Non-image word - render normally
RenderWordWithPreview(word, state, backgroundColor, quotesLeft, callbackUri, accountViewModel, nav)
RenderWordWithPreview(word, context)
i++
}
}
@@ -604,30 +597,25 @@ private fun RenderWordsWithImageGallery(
@Composable
private fun RenderWordWithPreview(
word: Segment,
state: RichTextViewerState,
backgroundColor: MutableState<Color>,
quotesLeft: Int,
callbackUri: String? = null,
accountViewModel: AccountViewModel,
nav: INav,
context: RenderContext,
) {
when (word) {
is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, callbackUri, accountViewModel)
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText, accountViewModel)
is WithdrawSegment -> MayBeWithdrawal(word.segmentText, accountViewModel)
is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
is ImageSegment -> ZoomableContentView(word.segmentText, context.state, context.accountViewModel)
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, context.callbackUri, context.accountViewModel)
is EmojiSegment -> RenderCustomEmoji(word.segmentText, context.state)
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText, context.accountViewModel)
is WithdrawSegment -> MayBeWithdrawal(word.segmentText, context.accountViewModel)
is CashuSegment -> CashuPreview(word.segmentText, context.accountViewModel)
is EmailSegment -> ClickableEmail(word.segmentText)
is SecretEmoji -> DisplaySecretEmoji(word, state, callbackUri, true, quotesLeft, backgroundColor, accountViewModel, nav)
is SecretEmoji -> DisplaySecretEmoji(word, context.state, context.callbackUri, true, context.quotesLeft, context.backgroundColor, context.accountViewModel, context.nav)
is PhoneSegment -> ClickablePhone(word.segmentText)
is BechSegment -> BechLink(word.segmentText, true, quotesLeft, backgroundColor, accountViewModel, nav)
is HashTagSegment -> HashTag(word, nav)
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
is HashIndexEventSegment -> TagLink(word, true, quotesLeft, backgroundColor, accountViewModel, nav)
is BechSegment -> BechLink(word.segmentText, true, context.quotesLeft, context.backgroundColor, context.accountViewModel, context.nav)
is HashTagSegment -> HashTag(word, context.nav)
is HashIndexUserSegment -> TagLink(word, context.accountViewModel, context.nav)
is HashIndexEventSegment -> TagLink(word, true, context.quotesLeft, context.backgroundColor, context.accountViewModel, context.nav)
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
is RegularTextSegment -> Text(word.segmentText)
is Base64Segment -> ZoomableContentView(word.segmentText, state, accountViewModel)
is Base64Segment -> ZoomableContentView(word.segmentText, context.state, context.accountViewModel)
}
}
@@ -786,15 +774,20 @@ fun CoreSecretMessage(
accountViewModel: AccountViewModel,
nav: INav,
) {
val context =
RenderContext(
state = localSecretContent,
backgroundColor = backgroundColor,
quotesLeft = quotesLeft,
callbackUri = callbackUri,
accountViewModel = accountViewModel,
nav = nav,
)
if (localSecretContent.paragraphs.size == 1) {
RenderWordsWithImageGallery(
localSecretContent.paragraphs[0].words.toImmutableList(),
localSecretContent,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
context,
)
} else if (localSecretContent.paragraphs.size > 1) {
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
@@ -807,12 +800,7 @@ fun CoreSecretMessage(
) {
RenderWordsWithImageGallery(
paragraph.words.toImmutableList(),
localSecretContent,
backgroundColor,
quotesLeft,
callbackUri,
accountViewModel,
nav,
context,
)
}
}