From 1f6d7d3fd2951a614006a8d3d9223d31c31ea74d Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 11 Sep 2025 20:58:15 +0200 Subject: [PATCH] Simplify RenderRegularWithGallery Simplify collectConsecutiveImageParagraphs: just return the end index Extracting common signature parameters into a RenderContext data class Extracting GalleryImage helper function --- .../amethyst/ui/components/ImageGallery.kt | 122 +++++----- .../amethyst/ui/components/RichTextViewer.kt | 210 +++++++++--------- 2 files changed, 167 insertions(+), 165 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt index 173a97ba4..caf820a26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt @@ -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, + 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, @@ -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, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index fb7442f25..acb71545c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -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, + 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, + 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, spaceWidth: Dp, - state: RichTextViewerState, - backgroundColor: MutableState, - 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, - state: RichTextViewerState, - backgroundColor: MutableState, - 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, - 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, ) } }