Hold a row's top edge during layout, so nothing is drawn out of place
The correction ran in a coroutine, so it landed a frame or more after the layout it was correcting: the wrong position was drawn once and then fixed, which reads as a flick and gets worse the faster the screen refreshes. That is a race with the display rather than a bug that can be tuned out, so the fix is not a shorter delay but a different phase. It now happens in the layout phase. `Modifier.holdTopEdge` learns the row's new height from the measurement that produced it and asks the list to shift by exactly that much, before anything is drawn. `requestScrollToItem` is the form that may be asked for during layout; `dispatchRawDelta` is not -- it calls forceRemeasure and dies with "performMeasureAndLayout called during measure layout", which cost one crash to establish. The arming flag and the per-row height are deliberately not snapshot state. Both are written from layout, where a snapshot write that composition reads would schedule another recomposition -- another frame, which is the thing being removed. This also drops the machinery the previous attempt needed: no waiting on a size change, no timeout, no marking the rows above to find one that could still report the move. A row measures itself, so a row that shrinks out of the viewport is no longer a special case. Verified with ui-trace sampling at ~1kHz, where a single bad frame would show as ten to twenty samples: expanding and collapsing from a heading are each one step from old position to held position with nothing in between, collapsing from the foot bar holds the four rows below it, a drag 120ms after a tap is left alone, and scrolling back stays put for six seconds. ktfmt, lint and 85 tests clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cab21442d6
commit
5eba6ec529
2 files changed
+173
-172
No files matched your search
@@ -628,14 +628,14 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens:
|
|||||||
a group's heading or the bar at its foot — is the end that stays put,
|
a group's heading or the bar at its foot — is the end that stays put,
|
||||||
and the row grows away from it. The transcript is laid out from the
|
and the row grows away from it. The transcript is laid out from the
|
||||||
bottom, so a row's bottom edge is anchored for free and the top one
|
bottom, so a row's bottom edge is anchored for free and the top one
|
||||||
has to be arranged. `toggleAnchored` waits on the row's *size*, which
|
has to be arranged. The correction lives in the *layout* phase
|
||||||
a scroll cannot change — a version that waited on the top edge instead
|
(`Modifier.holdTopEdge`): the measurement that discovers the row's new
|
||||||
mistook the reader's own drag for the row resizing and undid it — and
|
height asks the list to shift by that much, via
|
||||||
then reads how far things moved off any row from the pressed one
|
`requestScrollToItem`, before anything is drawn. Doing it from an
|
||||||
upwards, since a group taller than the screen disappears under the
|
effect instead means the wrong position is drawn once first, which
|
||||||
bottom edge as it shrinks and cannot report its own move (2026-08-30,
|
reads as a flick and gets worse the faster the screen refreshes
|
||||||
asked for after groups opened upwards and sent their own heading off
|
(2026-08-30, asked for after groups opened upwards and sent their own
|
||||||
the top of the screen).
|
heading off the top of the screen).
|
||||||
- Input bar: text, attach (camera/gallery/file), send — **always enabled**;
|
- Input bar: text, attach (camera/gallery/file), send — **always enabled**;
|
||||||
mid-run sends become steering messages.
|
mid-run sends become steering messages.
|
||||||
- Top bar: model chip (tap to change), stop button while running, token
|
- Top bar: model chip (tap to change), stop button while running, token
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.PickVisualMediaRequest
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.gestures.scrollBy
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -18,7 +17,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@@ -49,6 +47,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
@@ -63,10 +62,8 @@ import java.util.concurrent.atomic.AtomicReference
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
|
|
||||||
private const val RECONNECT_DELAY_MS = 1500L
|
private const val RECONNECT_DELAY_MS = 1500L
|
||||||
|
|
||||||
@@ -84,13 +81,50 @@ private const val RECONNECT_DELAY_MS = 1500L
|
|||||||
private const val HISTORY_LOOKAHEAD = 8
|
private const val HISTORY_LOOKAHEAD = 8
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long to keep holding a row's top edge while the row settles to its new height.
|
* Which row was asked to hold its top edge, and how tall it was when it last measured.
|
||||||
*
|
*
|
||||||
* Long enough for the second and third layout passes a row can take to reach its final height, and
|
* Deliberately *not* snapshot state, and that is the point of the whole class. Both fields are
|
||||||
* short enough that it is over before the reader could have started scrolling for their own
|
* written from the layout phase; a snapshot write there that composition reads would schedule
|
||||||
* reasons. It always runs to the end: there is no way to ask whether more passes are coming.
|
* another recomposition, and the correction has to land inside the frame that is already being laid
|
||||||
|
* out rather than in a later one. Nothing observes these, so nothing needs to.
|
||||||
|
*
|
||||||
|
* [key] is cleared by the resize it was set for, so it cannot be spent on an unrelated one.
|
||||||
*/
|
*/
|
||||||
private const val ANCHOR_SETTLE_MS = 500L
|
private class TopEdgeHold {
|
||||||
|
var key: Any? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One row's height between layouts, so a change in it can be noticed. See [holdTopEdge]. */
|
||||||
|
private class LastHeight {
|
||||||
|
var value: Int? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps this row's top edge where it is when the row changes height, if it was asked to.
|
||||||
|
*
|
||||||
|
* This runs in the *layout* phase, from the measurement that discovers the new height, and that is
|
||||||
|
* the whole reason it is a modifier rather than an effect. A correction posted to a coroutine
|
||||||
|
* arrives a frame or more after the layout it is correcting, so the wrong position is drawn once
|
||||||
|
* before the right one -- visible as a flick, and worse the faster the screen refreshes. Scrolling
|
||||||
|
* from here happens before anything is drawn, so there is no frame to see and nothing that depends
|
||||||
|
* on how quickly the correction is scheduled.
|
||||||
|
*
|
||||||
|
* [hold] is given the change in height. The row's bottom edge is held by the list, so a scroll of
|
||||||
|
* exactly that much is what leaves the top edge where it was.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun Modifier.holdTopEdge(key: Any, held: TopEdgeHold, hold: (Int) -> Unit): Modifier {
|
||||||
|
val last = remember { LastHeight() }
|
||||||
|
return onSizeChanged { size ->
|
||||||
|
val previous = last.value
|
||||||
|
last.value = size.height
|
||||||
|
// A first measurement has no previous height to have moved from, and a row that came
|
||||||
|
// back after being scrolled away is a first measurement again.
|
||||||
|
if (previous == null || previous == size.height || held.key != key) return@onSizeChanged
|
||||||
|
held.key = null
|
||||||
|
hold(size.height - previous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What the transcript renders: the event stream folded into displayable rows (see [foldEvent]). The
|
* What the transcript renders: the event stream folded into displayable rows (see [foldEvent]). The
|
||||||
@@ -490,6 +524,7 @@ fun SessionScreen(
|
|||||||
onSettings: () -> Unit,
|
onSettings: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val topEdgeHeld = remember { TopEdgeHold() }
|
||||||
var items by remember { mutableStateOf(listOf<TranscriptItem>()) }
|
var items by remember { mutableStateOf(listOf<TranscriptItem>()) }
|
||||||
var status by remember { mutableStateOf(summary.status) }
|
var status by remember { mutableStateOf(summary.status) }
|
||||||
// Seeded from the row this screen was opened from, so a conversation already under way says
|
// Seeded from the row this screen was opened from, so a conversation already under way says
|
||||||
@@ -692,72 +727,13 @@ fun SessionScreen(
|
|||||||
* up off the screen and fills the space above it, so the calls appear on the far side of the
|
* up off the screen and fills the space above it, so the calls appear on the far side of the
|
||||||
* control that produced them.
|
* control that produced them.
|
||||||
*
|
*
|
||||||
* So [RowEdge.Bottom] is the list's own behaviour and does nothing extra, and [RowEdge.Top]
|
* So [RowEdge.Bottom] is the list's own behaviour and needs nothing, while [RowEdge.Top] marks
|
||||||
* scrolls back by however much the row grew. It has to be measured rather than worked out: only
|
* the row as holding its top edge the next time it is measured. The correction itself belongs
|
||||||
* the layout knows how tall an open group is, and it depends on the calls in it.
|
* to the measurement -- see [holdTopEdge].
|
||||||
*
|
|
||||||
* What is measured is the row's *size*, and that is the whole reason this works. The obvious
|
|
||||||
* thing to measure is where its top edge went, and an earlier version did -- but a top edge
|
|
||||||
* also moves when the reader scrolls, so a correction still waiting for the layout would
|
|
||||||
* instead wake on the reader's own drag, read their scroll distance as the row's growth, and
|
|
||||||
* undo it. That was reported as the transcript jumping on every expand and refusing to scroll
|
|
||||||
* back at all. A size does not change when anybody scrolls, so the two cannot be confused.
|
|
||||||
*
|
|
||||||
* The row's bottom edge is held by the list, so scrolling by the growth is what puts the top
|
|
||||||
* edge back, and it is done for every step of that growth until [ANCHOR_SETTLE_MS] is up.
|
|
||||||
*/
|
*/
|
||||||
fun toggleAnchored(key: Any, edge: RowEdge, toggle: () -> Unit) {
|
fun toggleAnchored(key: Any, edge: RowEdge, toggle: () -> Unit) {
|
||||||
fun rowOf() = listState.layoutInfo.visibleItemsInfo.firstOrNull { it.key == key }
|
if (edge == RowEdge.Top) topEdgeHeld.key = key
|
||||||
val row = rowOf()
|
|
||||||
toggle()
|
toggle()
|
||||||
// Not on screen when it was pressed, so there is no edge of it to hold.
|
|
||||||
if (edge == RowEdge.Bottom || row == null) return
|
|
||||||
// Read out now, as numbers: the layout hands back a reused object per slot, so holding
|
|
||||||
// one and reading it later describes whatever item took that slot since.
|
|
||||||
val rowIndex = row.index
|
|
||||||
val rowSize = row.size
|
|
||||||
|
|
||||||
// How far the row's top edge moved, read off any row that moved with it. The row and
|
|
||||||
// everything above it shift by exactly the row's growth, so they all give the same
|
|
||||||
// answer -- and several are recorded because which of them can answer varies. A row
|
|
||||||
// that shrinks slides down behind its own anchored bottom edge, and a group taller
|
|
||||||
// than the screen goes under the bottom of it entirely, leaving nothing of itself to
|
|
||||||
// measure.
|
|
||||||
fun edgeOf(item: LazyListItemInfo) =
|
|
||||||
if (item.index == rowIndex) item.offset + item.size else item.offset
|
|
||||||
val marks =
|
|
||||||
listState.layoutInfo.visibleItemsInfo
|
|
||||||
.filter { it.index >= rowIndex }
|
|
||||||
.associate { it.key to edgeOf(it) }
|
|
||||||
|
|
||||||
scope.launch {
|
|
||||||
// Waited on by *size*, which is the one thing a scroll cannot change. An earlier
|
|
||||||
// version waited on the top edge instead, so a correction still pending would wake
|
|
||||||
// on the reader's own drag, read their scroll distance as the row's growth and undo
|
|
||||||
// it -- reported as the transcript jumping on every expand and refusing to scroll
|
|
||||||
// back at all.
|
|
||||||
//
|
|
||||||
// The wait answers `true` rather than the size it saw, because the size it saw is
|
|
||||||
// sometimes `null` -- that is the row leaving the screen, which is a real answer and
|
|
||||||
// the one this has to handle. Returned straight out of `withTimeoutOrNull` it would
|
|
||||||
// be the same value that means "never happened", and the case needing the correction
|
|
||||||
// most was the case silently skipped.
|
|
||||||
val settled =
|
|
||||||
withTimeoutOrNull(ANCHOR_SETTLE_MS) {
|
|
||||||
snapshotFlow { rowOf()?.size }.first { it != rowSize }
|
|
||||||
true
|
|
||||||
}
|
|
||||||
if (settled == null) return@launch
|
|
||||||
// Never against a finger already on the screen: the reader is placing the list
|
|
||||||
// themselves, and that is also the only way a mark could have moved for a reason
|
|
||||||
// other than the row resizing.
|
|
||||||
if (listState.isScrollInProgress) return@launch
|
|
||||||
val moved =
|
|
||||||
listState.layoutInfo.visibleItemsInfo.firstNotNullOfOrNull { item ->
|
|
||||||
marks[item.key]?.let { edgeOf(item) - it }
|
|
||||||
} ?: return@launch
|
|
||||||
listState.scrollBy(moved.toFloat())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A compaction reports nothing about its own progress -- measured against the CLI, which
|
// A compaction reports nothing about its own progress -- measured against the CLI, which
|
||||||
@@ -1211,107 +1187,132 @@ fun SessionScreen(
|
|||||||
// at the same end. Paging older history is the opposite insertion and was
|
// at the same end. Paging older history is the opposite insertion and was
|
||||||
// already fine, and stays fine, because a key survives both.
|
// already fine, and stays fine, because a key survives both.
|
||||||
items(rows.asReversed(), key = { it.key }) { row ->
|
items(rows.asReversed(), key = { it.key }) { row ->
|
||||||
when (row) {
|
Box(
|
||||||
is TranscriptRow.Tools ->
|
Modifier.holdTopEdge(row.key, topEdgeHeld) { grew ->
|
||||||
ToolGroup(
|
// Requested rather than scrolled. Scrolling forces a remeasure,
|
||||||
group = row,
|
// and forcing one from inside a measure throws; this is the form
|
||||||
expanded = row.id in expandedGroups,
|
// built to be asked for during layout and applied in that pass.
|
||||||
onToggle = { edge ->
|
listState.requestScrollToItem(
|
||||||
toggleAnchored(row.key, edge) {
|
listState.firstVisibleItemIndex,
|
||||||
expandedGroups =
|
listState.firstVisibleItemScrollOffset + grew,
|
||||||
if (row.id in expandedGroups) expandedGroups - row.id
|
|
||||||
else expandedGroups + row.id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isToolExpanded = { it in expandedTools },
|
|
||||||
// Anchored on the group, not the call: opening one call makes
|
|
||||||
// the whole group taller, and the heading the reader is under
|
|
||||||
// is the group's.
|
|
||||||
onToolToggle = { id ->
|
|
||||||
toggleAnchored(row.key, RowEdge.Top) {
|
|
||||||
expandedTools =
|
|
||||||
if (id in expandedTools) expandedTools - id
|
|
||||||
else expandedTools + id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onAnswer = { questionId, answers ->
|
|
||||||
act {
|
|
||||||
answerQuestion(settings, summary.id, questionId, answers)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
image = { ref -> SessionImage(settings, summary.id, ref) },
|
|
||||||
)
|
)
|
||||||
is TranscriptRow.Single ->
|
}
|
||||||
when (val item = row.item) {
|
) {
|
||||||
is TranscriptItem.UserMsg ->
|
when (row) {
|
||||||
UserBubble(
|
is TranscriptRow.Tools ->
|
||||||
settings = settings,
|
ToolGroup(
|
||||||
sessionId = summary.id,
|
group = row,
|
||||||
text = item.text,
|
expanded = row.id in expandedGroups,
|
||||||
images = item.images,
|
onToggle = { edge ->
|
||||||
)
|
toggleAnchored(row.key, edge) {
|
||||||
is TranscriptItem.AssistantMsg -> AssistantMessage(item.text)
|
expandedGroups =
|
||||||
is TranscriptItem.ToolRun ->
|
if (row.id in expandedGroups)
|
||||||
ToolCard(
|
expandedGroups - row.id
|
||||||
tool = item,
|
else expandedGroups + row.id
|
||||||
expanded = item.id in expandedTools,
|
}
|
||||||
onToggle = {
|
},
|
||||||
toggleAnchored(row.key, RowEdge.Top) {
|
isToolExpanded = { it in expandedTools },
|
||||||
expandedTools =
|
// Anchored on the group, not the call: opening one call makes
|
||||||
if (item.id in expandedTools)
|
// the whole group taller, and the heading the reader is under
|
||||||
expandedTools - item.id
|
// is the group's.
|
||||||
else expandedTools + item.id
|
onToolToggle = { id ->
|
||||||
}
|
toggleAnchored(row.key, RowEdge.Top) {
|
||||||
},
|
expandedTools =
|
||||||
onAnswer = { questionId, answers ->
|
if (id in expandedTools) expandedTools - id
|
||||||
|
else expandedTools + id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAnswer = { questionId, answers ->
|
||||||
|
act {
|
||||||
|
answerQuestion(
|
||||||
|
settings,
|
||||||
|
summary.id,
|
||||||
|
questionId,
|
||||||
|
answers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
image = { ref -> SessionImage(settings, summary.id, ref) },
|
||||||
|
)
|
||||||
|
is TranscriptRow.Single ->
|
||||||
|
when (val item = row.item) {
|
||||||
|
is TranscriptItem.UserMsg ->
|
||||||
|
UserBubble(
|
||||||
|
settings = settings,
|
||||||
|
sessionId = summary.id,
|
||||||
|
text = item.text,
|
||||||
|
images = item.images,
|
||||||
|
)
|
||||||
|
is TranscriptItem.AssistantMsg -> AssistantMessage(item.text)
|
||||||
|
is TranscriptItem.ToolRun ->
|
||||||
|
ToolCard(
|
||||||
|
tool = item,
|
||||||
|
expanded = item.id in expandedTools,
|
||||||
|
onToggle = {
|
||||||
|
toggleAnchored(row.key, RowEdge.Top) {
|
||||||
|
expandedTools =
|
||||||
|
if (item.id in expandedTools)
|
||||||
|
expandedTools - item.id
|
||||||
|
else expandedTools + item.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAnswer = { questionId, answers ->
|
||||||
|
act {
|
||||||
|
answerQuestion(
|
||||||
|
settings,
|
||||||
|
summary.id,
|
||||||
|
questionId,
|
||||||
|
answers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
image = { ref ->
|
||||||
|
SessionImage(settings, summary.id, ref)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
is TranscriptItem.QuestionCard ->
|
||||||
|
QuestionRow(item) { answers ->
|
||||||
act {
|
act {
|
||||||
answerQuestion(
|
answerQuestion(
|
||||||
settings,
|
settings,
|
||||||
summary.id,
|
summary.id,
|
||||||
questionId,
|
item.id,
|
||||||
answers,
|
answers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
image = { ref -> SessionImage(settings, summary.id, ref) },
|
|
||||||
)
|
|
||||||
is TranscriptItem.QuestionCard ->
|
|
||||||
QuestionRow(item) { answers ->
|
|
||||||
act {
|
|
||||||
answerQuestion(settings, summary.id, item.id, answers)
|
|
||||||
}
|
}
|
||||||
}
|
is TranscriptItem.ErrorMsg ->
|
||||||
is TranscriptItem.ErrorMsg ->
|
Text(
|
||||||
Text(
|
item.message,
|
||||||
item.message,
|
color = MaterialTheme.colorScheme.error,
|
||||||
color = MaterialTheme.colorScheme.error,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
)
|
||||||
)
|
is TranscriptItem.ImageItem ->
|
||||||
is TranscriptItem.ImageItem ->
|
SessionImage(settings, summary.id, item.ref)
|
||||||
SessionImage(settings, summary.id, item.ref)
|
is TranscriptItem.Note ->
|
||||||
is TranscriptItem.Note ->
|
Text(
|
||||||
Text(
|
item.text,
|
||||||
item.text,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
)
|
||||||
)
|
is TranscriptItem.CommandRow -> CommandBubble(item.text)
|
||||||
is TranscriptItem.CommandRow -> CommandBubble(item.text)
|
is TranscriptItem.ClearedNote -> ClearedRow()
|
||||||
is TranscriptItem.ClearedNote -> ClearedRow()
|
is TranscriptItem.CompactedNote -> CompactedRow(item)
|
||||||
is TranscriptItem.CompactedNote -> CompactedRow(item)
|
is TranscriptItem.PeerNote ->
|
||||||
is TranscriptItem.PeerNote ->
|
PeerMessageRow(
|
||||||
PeerMessageRow(
|
item = item,
|
||||||
item = item,
|
expanded = item.seq in expandedNotes,
|
||||||
expanded = item.seq in expandedNotes,
|
onToggle = {
|
||||||
onToggle = {
|
toggleAnchored(row.key, RowEdge.Top) {
|
||||||
toggleAnchored(row.key, RowEdge.Top) {
|
expandedNotes =
|
||||||
expandedNotes =
|
if (item.seq in expandedNotes)
|
||||||
if (item.seq in expandedNotes)
|
expandedNotes - item.seq
|
||||||
expandedNotes - item.seq
|
else expandedNotes + item.seq
|
||||||
else expandedNotes + item.seq
|
}
|
||||||
}
|
},
|
||||||
},
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user