diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/NoteComposeLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/NoteComposeLayout.kt new file mode 100644 index 000000000..ec6a9e72d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/NoteComposeLayout.kt @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.layouts + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.ui.theme.Size55dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn + +/** + * Pre-computed pixel dimensions for [NoteComposeLayout], cached via [remember] + * so dp-to-px conversion only runs once per density change. + */ +@Immutable +private data class NoteLayoutPx( + /** Author picture column width: 55dp */ + val authorWidth: Int, + /** Horizontal gap between author column and content column: 10dp */ + val authorContentGap: Int, + /** Vertical gap between author picture and relay badges: 5dp */ + val authorBadgeGap: Int, + /** Vertical spacer between header rows and note content: 4dp */ + val contentSpacer: Int, + /** Left padding for the content area: 12dp */ + val padStart: Int, + /** Right padding for the content area: 12dp */ + val padEnd: Int, + /** Top padding for the content area: 10dp */ + val padTop: Int, +) + +@Composable +@Preview +private fun NoteComposeLayoutPreview() { + ThemeComparisonColumn { + NoteComposeLayoutPreviewCard() + } +} + +@Composable +private fun NoteComposeLayoutPreviewCard() { + NoteComposeLayout( + modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surface), + showSecondRow = true, + authorPicture = { + Box( + Modifier + .size(55.dp) + .clip(CircleShape) + .background(Color(0xFF9575CD)), + ) + }, + relayBadges = { + Text("R1 R2 R3", fontSize = 10.sp, color = Color.Gray) + }, + firstRow = { + Text( + "Alice @alice 2h ...", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + secondRow = { + Text( + "alice@nostr.com", + fontSize = 12.sp, + color = Color.Gray, + maxLines = 1, + ) + }, + noteContent = { + Text( + "This is a sample note content that demonstrates the custom " + + "NoteComposeLayout with all slots populated. The layout handles " + + "author picture, relay badges, header rows, content, and reactions.", + ) + }, + reactionsRow = { + Text(" Reply 3 Boost 5 Like 12 Zap 1.2k", fontSize = 12.sp, color = Color.Gray) + }, + ) +} + +@Composable +@Preview +private fun NoteComposeLayoutBoostedPreview() { + ThemeComparisonColumn { + NoteComposeLayout( + modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surface), + addPadding = false, + showAuthorColumn = false, + authorPicture = {}, + relayBadges = {}, + firstRow = { + Text("Bob boosted 2h ...") + }, + secondRow = {}, + noteContent = { + Text("Boosted note content without author column or padding.") + }, + reactionsRow = { + Text(" Reply Boost Like Zap", fontSize = 12.sp, color = Color.Gray) + }, + ) + } +} + +/** + * Custom single-pass layout for note feed items that replaces the nested + * Row/Column structure with direct constraint calculation using known fixed + * dimensions. + * + * ## Layout structure + * + * When [showAuthorColumn] is true (normal notes): + * ``` + * ┌──────────────────────────────────────────────┐ + * │ padding (start=12, end=12, top=10) │ + * │ ┌──────┐ 10dp ┌────────────────────────────┐ │ + * │ │author│ gap │ firstRow │ │ + * │ │ 55dp │ │ secondRow (optional) │ │ + * │ │ │ │ 4dp spacer (optional) │ │ + * │ ├──────┤ 5dp │ noteContent │ │ + * │ │relay │ gap │ │ │ + * │ │badges│ │ │ │ + * │ └──────┘ └────────────────────────────┘ │ + * ├───────────────────────────────────────────────┤ + * │ reactionsRow (full width, no side padding) │ + * └───────────────────────────────────────────────┘ + * ``` + * + * When [showAuthorColumn] is false (boosted/quoted notes): + * ``` + * ┌───────────────────────────────────────────────┐ + * │ firstRow (full width) │ + * │ noteContent │ + * ├───────────────────────────────────────────────┤ + * │ reactionsRow (full width) │ + * └───────────────────────────────────────────────┘ + * ``` + * + * ## Design choices + * + * **Multi-content Layout instead of layoutId**: Uses [Layout] with `contents: List` + * so each slot's composables become direct measurables without Box/Column wrappers. + * This eliminates 6 intermediate layout nodes per note compared to the layoutId + * approach. Empty slot lambdas produce empty measurable lists, which are skipped + * entirely (no [Constraints.fixed] zero-size measurement needed). + * + * **Padding in measure policy instead of Modifier.padding()**: The reactions row + * spans the full parent width without side padding, while the author + content area + * has 12dp horizontal padding. A single Modifier.padding() can't express different + * padding for different children, so padding offsets are applied during placement. + * + * **placeRelative instead of place**: Ensures correct positioning in RTL layouts + * by automatically mirroring x coordinates. + * + * **arrayOfNulls instead of map**: For multi-child slots (noteContent, reactionsRow), + * uses pre-sized arrays with indexed iteration to avoid List allocation overhead in + * the measure pass hot path. + * + * ## Slot ordering contract + * + * The `contents` list and `allMeasurables` indices are: + * - 0: [authorPicture] - 0 or 1 measurables + * - 1: [relayBadges] - 0 or 1 measurables + * - 2: [firstRow] - exactly 1 measurable + * - 3: [secondRow] - 0 or 1 measurables + * - 4: [noteContent] - 1+ measurables (stacked vertically) + * - 5: [reactionsRow] - 0+ measurables (stacked vertically) + * + * ## Performance + * + * Compared to the previous nested Row > Column > Column structure: + * - Eliminates 3 layout node levels (Row, author Column, content Column) + * - Eliminates Row's two-pass measurement (measure author first, then content) + * - Pre-computes all pixel dimensions once via [remember], cached across recompositions + * - Remaining per-frame allocations: ~48 bytes for `listOf(6 lambdas)` + + * ~24 bytes per `arrayOfNulls` for multi-child slots + * + * The caller should use `drawBehind { drawRect(color) }` instead of + * `background(color)` on the [modifier] to avoid recomposition when the + * background color state changes (e.g. "new item" highlight fade). + * + * @param modifier Applied to the Layout root. Typically includes combinedClickable + * for note navigation, drawBehind for background color, and fillMaxWidth. + * @param addPadding Whether to add standard note padding (12dp sides, 10dp top) + * to the author + content area. False for boosted notes. + * @param showAuthorColumn Whether to reserve 55dp for the author picture column + * with a 10dp gap. False for boosted/quoted notes. + * @param showSecondRow Whether to measure and place the second header row + * (NIP-05 status, location, PoW). Requires complete UI mode. + * @param showContentSpacer Whether to add a 4dp spacer between header rows and + * note content. False for repost events. + * @param authorPicture Slot for the author's profile picture (55x55dp area). + * Should emit nothing when [showAuthorColumn] is false to skip composition. + * @param relayBadges Slot for relay indicator icons below the author picture. + * Should emit nothing when [showAuthorColumn] is false. + * @param firstRow Slot for the primary header: author name, time, more options. + * Always present. + * @param secondRow Slot for the secondary header: NIP-05, location, PoW, OTS. + * Should emit nothing when [showSecondRow] is false. + * @param noteContent Slot for the event-specific content. May emit multiple + * children (content + zap splits + approval button) which are stacked vertically. + * @param reactionsRow Slot for the reaction buttons row. May emit a ReactionsRow, + * a Spacer, or nothing depending on event type. + */ +@Composable +fun NoteComposeLayout( + modifier: Modifier = Modifier, + addPadding: Boolean = true, + showAuthorColumn: Boolean = true, + showSecondRow: Boolean = false, + showContentSpacer: Boolean = true, + authorPicture: @Composable () -> Unit, + relayBadges: @Composable () -> Unit, + firstRow: @Composable () -> Unit, + secondRow: @Composable () -> Unit, + noteContent: @Composable () -> Unit, + reactionsRow: @Composable () -> Unit, +) { + val density = LocalDensity.current + val px = + remember(density) { + with(density) { + NoteLayoutPx( + authorWidth = Size55dp.roundToPx(), + authorContentGap = 10.dp.roundToPx(), + authorBadgeGap = 5.dp.roundToPx(), + contentSpacer = 4.dp.roundToPx(), + padStart = 12.dp.roundToPx(), + padEnd = 12.dp.roundToPx(), + padTop = 10.dp.roundToPx(), + ) + } + } + + // Slot order must match the indices used in the measure policy below. + Layout( + contents = + listOf( + authorPicture, // 0 + relayBadges, // 1 + firstRow, // 2 + secondRow, // 3 + noteContent, // 4 + reactionsRow, // 5 + ), + modifier = modifier, + ) { allMeasurables, constraints -> + val maxWidth = constraints.maxWidth + + // Padding is applied in the measure policy (not via Modifier.padding) because + // the reactions row needs full width while the content area is inset. + val padStart = if (addPadding) px.padStart else 0 + val padEnd = if (addPadding) px.padEnd else 0 + val padTop = if (addPadding) px.padTop else 0 + + // Column widths are computed from known fixed dimensions, eliminating + // the Row's two-pass measurement (measure author → compute remaining → measure content). + val innerWidth = maxWidth - padStart - padEnd + val authorW = if (showAuthorColumn) px.authorWidth else 0 + val gap = if (showAuthorColumn) px.authorContentGap else 0 + val contentWidth = (innerWidth - authorW - gap).coerceAtLeast(0) + + // Constraints is a value class (inline Long) so these don't allocate. + val authorConstraints = Constraints(maxWidth = px.authorWidth) + val contentConstraints = Constraints(maxWidth = contentWidth) + val fullWidthConstraints = Constraints(maxWidth = maxWidth) + + // Single-child slots: firstOrNull() returns null for empty slots (hidden), + // skipping measurement entirely. + val authorPlaceable = allMeasurables[0].firstOrNull()?.measure(authorConstraints) + val relayPlaceable = allMeasurables[1].firstOrNull()?.measure(authorConstraints) + + val firstRowPlaceable = allMeasurables[2].firstOrNull()?.measure(contentConstraints) + val secondRowPlaceable = + if (showSecondRow) allMeasurables[3].firstOrNull()?.measure(contentConstraints) else null + + // Multi-child slots: measured into pre-sized arrays to avoid List allocation. + val contentMeasurables = allMeasurables[4] + val contentPlaceables = arrayOfNulls(contentMeasurables.size) + var contentStackHeight = 0 + for (i in contentMeasurables.indices) { + val placeable = contentMeasurables[i].measure(contentConstraints) + contentPlaceables[i] = placeable + contentStackHeight += placeable.height + } + + val reactionsMeasurables = allMeasurables[5] + val reactionsPlaceables = arrayOfNulls(reactionsMeasurables.size) + var reactionsHeight = 0 + for (i in reactionsMeasurables.indices) { + val placeable = reactionsMeasurables[i].measure(fullWidthConstraints) + reactionsPlaceables[i] = placeable + reactionsHeight += placeable.height + } + + // Height calculation + val spacer = if (showContentSpacer) px.contentSpacer else 0 + + val contentColumnHeight = + (firstRowPlaceable?.height ?: 0) + + (secondRowPlaceable?.height ?: 0) + + spacer + + contentStackHeight + + val authorColumnHeight = + if (showAuthorColumn && authorPlaceable != null) { + authorPlaceable.height + px.authorBadgeGap + (relayPlaceable?.height ?: 0) + } else { + 0 + } + + val mainAreaHeight = maxOf(authorColumnHeight, contentColumnHeight) + val totalHeight = padTop + mainAreaHeight + reactionsHeight + + // placeRelative handles RTL by mirroring x coordinates automatically. + layout(maxWidth, totalHeight.coerceAtLeast(constraints.minHeight)) { + // Author column (inside padding area) + if (showAuthorColumn) { + authorPlaceable?.placeRelative(padStart, padTop) + relayPlaceable?.placeRelative( + padStart, + padTop + (authorPlaceable?.height ?: 0) + px.authorBadgeGap, + ) + } + + // Content column (to the right of author, inside padding area) + val contentX = padStart + authorW + gap + var y = padTop + + firstRowPlaceable?.placeRelative(contentX, y) + y += firstRowPlaceable?.height ?: 0 + + if (showSecondRow) { + secondRowPlaceable?.placeRelative(contentX, y) + y += secondRowPlaceable?.height ?: 0 + } + + y += spacer + + for (placeable in contentPlaceables) { + placeable?.placeRelative(contentX, y) + y += placeable?.height ?: 0 + } + + // Reactions row (full width, no side padding, below main area) + var reactionsY = padTop + mainAreaHeight + for (placeable in reactionsPlaceables) { + placeable?.placeRelative(0, reactionsY) + reactionsY += placeable?.height ?: 0 + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 320033df7..0e89d1199 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -51,6 +51,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver @@ -70,6 +71,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.layouts.GenericRepostLayout +import com.vitorpamplona.amethyst.ui.layouts.NoteComposeLayout import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor @@ -178,8 +180,6 @@ import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier -import com.vitorpamplona.amethyst.ui.theme.RowColSpacing10dp -import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.Size30dp @@ -189,12 +189,9 @@ import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.UserNameMaxRowHeight import com.vitorpamplona.amethyst.ui.theme.UserNameRowHeight -import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier -import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor -import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent @@ -550,33 +547,74 @@ private fun CheckNewAndRenderNote( accountViewModel, ) - ClickableNote( + InnerNoteWithReactions( baseNote = baseNote, backgroundColor = backgroundColor, - modifier = modifier, + clickModifier = clickableNoteModifier(baseNote, modifier, accountViewModel, showPopup, nav), + isBoostedNote = isBoostedNote, + isQuotedNote = isQuotedNote, + unPackReply = unPackReply, + makeItShort = makeItShort, + canPreview = canPreview, + isPinned = isPinned, + quotesLeft = quotesLeft, accountViewModel = accountViewModel, - showPopup = showPopup, nav = nav, - ) { - InnerNoteWithReactions( - baseNote = baseNote, - backgroundColor = backgroundColor, - isBoostedNote = isBoostedNote, - isQuotedNote = isQuotedNote, - unPackReply = unPackReply, - makeItShort = makeItShort, - canPreview = canPreview, - isPinned = isPinned, - quotesLeft = quotesLeft, - accountViewModel = accountViewModel, - nav = nav, - moreOptions = moreOptions, - ) - } + moreOptions = moreOptions, + ) } +/** + * Creates a [Modifier] with [combinedClickable] for note navigation (click to open, + * long-press for quick action popup). Remembered by [baseNote] and [modifier] to avoid + * recreating the click handler on every recomposition. + * + * Used directly on [NoteComposeLayout]'s modifier in [InnerNoteWithReactions] to avoid + * a wrapping Column node. For callers that need a wrapping container, use [ClickableNote]. + */ @Composable @OptIn(ExperimentalFoundationApi::class) +fun clickableNoteModifier( + baseNote: Note, + modifier: Modifier, + accountViewModel: AccountViewModel, + showPopup: () -> Unit, + nav: INav, +): Modifier = + remember(baseNote, modifier) { + modifier + .combinedClickable( + onClick = { + val redirectToNote = + if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) { + baseNote.replyTo?.lastOrNull() ?: baseNote + } else { + baseNote + } + + nav.nav { + if (redirectToNote.event is DraftWrapEvent) { + withContext(Dispatchers.IO) { + routeEditDraftTo(redirectToNote, accountViewModel.account) + } + } else { + routeFor(redirectToNote, accountViewModel.account) + } + } + }, + onLongClick = showPopup, + ) + } + +/** + * Wraps [content] in a Column with click handling and animated background. + * + * Kept for callers outside NoteCompose (e.g. ChannelCardCompose) that need a + * container for non-NoteComposeLayout content. For NoteCompose itself, + * [InnerNoteWithReactions] applies [clickableNoteModifier] directly on the + * [NoteComposeLayout] modifier to eliminate this Column wrapper node. + */ +@Composable fun ClickableNote( baseNote: Note, modifier: Modifier, @@ -586,39 +624,20 @@ fun ClickableNote( nav: INav, content: @Composable () -> Unit, ) { - val updatedModifier = - remember(baseNote, modifier) { - modifier - .combinedClickable( - onClick = { - val redirectToNote = - if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) { - baseNote.replyTo?.lastOrNull() ?: baseNote - } else { - baseNote - } - - nav.nav { - if (redirectToNote.event is DraftWrapEvent) { - withContext(Dispatchers.IO) { - routeEditDraftTo(redirectToNote, accountViewModel.account) - } - } else { - routeFor(redirectToNote, accountViewModel.account) - } - } - }, - onLongClick = showPopup, - ) - } - - Column(modifier = updatedModifier.background(backgroundColor.value)) { content() } + Column( + modifier = + clickableNoteModifier(baseNote, modifier, accountViewModel, showPopup, nav) + .background(backgroundColor.value), + ) { + content() + } } @Composable fun InnerNoteWithReactions( baseNote: Note, backgroundColor: MutableState, + clickModifier: Modifier = Modifier, isBoostedNote: Boolean, isQuotedNote: Boolean, unPackReply: ReplyRenderType, @@ -632,74 +651,94 @@ fun InnerNoteWithReactions( ) { val notBoostedNorQuote = !isBoostedNote && !isQuotedNote val editState = observeEdits(baseNote = baseNote, accountViewModel = accountViewModel) + val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent + val showSecondRow = isNotRepost && notBoostedNorQuote && accountViewModel.settings.isCompleteUIMode() - Row( + NoteComposeLayout( modifier = - if (!isBoostedNote) { - normalWithTopMarginNoteModifier - } else { - boostedNoteModifier - }, - horizontalArrangement = RowColSpacing10dp, - ) { - if (notBoostedNorQuote) { - Column(WidthAuthorPictureModifier, verticalArrangement = RowColSpacing5dp) { - // Draws the boosted picture outside the boosted card. + clickModifier + .drawBehind { drawRect(backgroundColor.value) } + .fillMaxWidth(), + addPadding = !isBoostedNote, + showAuthorColumn = notBoostedNorQuote, + showSecondRow = showSecondRow, + showContentSpacer = isNotRepost, + authorPicture = { + if (notBoostedNorQuote) { Box(modifier = Size55Modifier, contentAlignment = Alignment.BottomEnd) { RenderAuthorImages(baseNote, nav, accountViewModel) } - + } + }, + relayBadges = { + if (notBoostedNorQuote) { BadgeBox(baseNote, accountViewModel, nav) } - } - - Column(Modifier.fillMaxWidth()) { - val showSecondRow = - baseNote.event !is RepostEvent && - baseNote.event !is GenericRepostEvent && - !isBoostedNote && - !isQuotedNote && - accountViewModel.settings.isCompleteUIMode() - NoteBody( + }, + firstRow = { + FirstUserInfoRow( baseNote = baseNote, showAuthorPicture = isQuotedNote, - unPackReply = unPackReply, - makeItShort = makeItShort, - canPreview = canPreview, - showSecondRow = showSecondRow, isPinned = isPinned, - quotesLeft = quotesLeft, - backgroundColor = backgroundColor, editState = editState, accountViewModel = accountViewModel, nav = nav, moreOptions = moreOptions, ) - - RenderApprovalIfNeeded(baseNote, accountViewModel, nav) - } - } - - val isNotRepost = baseNote.event !is RepostEvent && baseNote.event !is GenericRepostEvent && baseNote.event !is DraftWrapEvent - - if (isNotRepost) { - if (makeItShort) { - Spacer(modifier = DoubleVertSpacer) - } else { - ReactionsRow( + }, + secondRow = { + if (showSecondRow) { + SecondUserInfoRow( + baseNote, + editState, + accountViewModel, + nav, + ) + } + }, + noteContent = { + RenderNoteRow( baseNote = baseNote, - showReactionDetail = notBoostedNorQuote, - addPadding = !isBoostedNote, + backgroundColor = backgroundColor, + makeItShort = makeItShort, + canPreview = canPreview, editState = editState, + quotesLeft = quotesLeft, + unPackReply = unPackReply, accountViewModel = accountViewModel, nav = nav, ) - } - } else { - if (baseNote.event is DraftWrapEvent) { - Spacer(modifier = DoubleVertSpacer) - } - } + + if (!makeItShort) { + val noteEvent = baseNote.event + val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false } + if (zapSplits && noteEvent != null) { + Spacer(modifier = HalfDoubleVertSpacer) + DisplayZapSplits(noteEvent, false, accountViewModel, nav) + } + } + + RenderApprovalIfNeeded(baseNote, accountViewModel, nav) + }, + reactionsRow = { + if (isNotRepost && baseNote.event !is DraftWrapEvent) { + if (makeItShort) { + Spacer(modifier = DoubleVertSpacer) + } else { + ReactionsRow( + baseNote = baseNote, + showReactionDetail = notBoostedNorQuote, + addPadding = !isBoostedNote, + editState = editState, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } else if (baseNote.event is DraftWrapEvent) { + Spacer(modifier = DoubleVertSpacer) + } + }, + ) } @Composable