mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
fix(mobile): forget queued-row heights for messages that left the queue
The queued list cached each row's height by message id for the life of the chat, so a long conversation kept one entry per message ever queued and a requeued id could place a failed row by the height it left with. Keep heights only for queued ids, and drop a layout a departed row reports late.
This commit is contained in:
@@ -431,6 +431,60 @@ describe('MobileNativeChatQueuedMessages', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('places a failed row by the height a returning message lays out at, not the one it left with', async () => {
|
||||
const scrollTo = vi.fn()
|
||||
const mounted = create(createElement('View'), {
|
||||
createNodeMock: (node) =>
|
||||
node.type === 'ScrollView'
|
||||
? { scrollTo, scrollToEnd: vi.fn(), flashScrollIndicators: vi.fn() }
|
||||
: null
|
||||
})
|
||||
renderer = mounted
|
||||
// Rows render in queue order.
|
||||
const layRow = async (index: number, height: number) => {
|
||||
const row = rows(mounted)[index]!
|
||||
await act(async () => row.props.onLayout({ nativeEvent: { layout: { height } } }))
|
||||
}
|
||||
await act(async () =>
|
||||
mounted.update(
|
||||
element({
|
||||
cards: [card({ messageId: 'a' }), card({ messageId: 'b' }), card({ messageId: 'c' })]
|
||||
})
|
||||
)
|
||||
)
|
||||
const scroll = mounted.root.find((node) => String(node.type) === 'ScrollView')
|
||||
await act(async () => {
|
||||
scroll.props.onLayout({ nativeEvent: { layout: { height: 100 } } })
|
||||
scroll.props.onContentSizeChange(0, 300)
|
||||
})
|
||||
// b leaves tall (an open editor or expanded text), then comes back as a plain 60pt row.
|
||||
await layRow(0, 60)
|
||||
await layRow(1, 180)
|
||||
await layRow(2, 60)
|
||||
await act(async () =>
|
||||
mounted.update(element({ cards: [card({ messageId: 'a' }), card({ messageId: 'c' })] }))
|
||||
)
|
||||
scrollTo.mockClear()
|
||||
await act(async () =>
|
||||
mounted.update(
|
||||
element({
|
||||
cards: [
|
||||
card({ messageId: 'a' }),
|
||||
card({ messageId: 'b' }),
|
||||
card({ messageId: 'c', needsAttention: true, caption: "Couldn't send" })
|
||||
]
|
||||
})
|
||||
)
|
||||
)
|
||||
// The failed row reports before b does; b's old height must not place it.
|
||||
await layRow(2, 60)
|
||||
expect(scrollTo).not.toHaveBeenCalled()
|
||||
await layRow(1, 60)
|
||||
// c spans 120–180 of a 100pt list: the nearest scroll that shows it, clear of the edge fade.
|
||||
expect(scrollTo).toHaveBeenCalledTimes(1)
|
||||
expect(scrollTo).toHaveBeenCalledWith({ y: 96, animated: false })
|
||||
})
|
||||
|
||||
it('scrolls to a failure the list mounts open for, and settles when no row lays out again', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
|
||||
@@ -31,6 +31,10 @@ export const QUEUED_ROW_SHOW_SETTLE_MS = 300
|
||||
|
||||
type ShowRow = (row: LayoutRectangle) => void
|
||||
|
||||
// Message ids cannot contain NUL.
|
||||
const ID_SEPARATOR = '\u0000'
|
||||
const NO_IDS: ReadonlySet<string> = new Set()
|
||||
|
||||
/**
|
||||
* Scrolls the queue to one row (a failure that opened the list) once that row has laid out again.
|
||||
* A row whose height never changes reports no layout, so the request settles shortly after either way.
|
||||
@@ -51,6 +55,20 @@ export function useQueuedRowShow(
|
||||
useLayoutEffect(() => {
|
||||
order.current = messageIds
|
||||
})
|
||||
// Only queued rows keep a height, so a long conversation holds none for messages that left, and
|
||||
// an id that comes back (a requeued draft) waits for its own fresh layout.
|
||||
const liveKey = messageIds.join(ID_SEPARATOR)
|
||||
// Filled before any row lays out: layout effects run before native layout events.
|
||||
const live = useRef(NO_IDS)
|
||||
useLayoutEffect(() => {
|
||||
live.current = new Set(liveKey ? liveKey.split(ID_SEPARATOR) : [])
|
||||
for (const messageId of heights.current.keys()) {
|
||||
if (!live.current.has(messageId)) {
|
||||
heights.current.delete(messageId)
|
||||
fresh.current.delete(messageId)
|
||||
}
|
||||
}
|
||||
}, [liveKey])
|
||||
useLayoutEffect(() => {
|
||||
fresh.current.clear()
|
||||
}, [showId])
|
||||
@@ -73,6 +91,10 @@ export function useQueuedRowShow(
|
||||
height: number,
|
||||
show: ShowRow
|
||||
) => {
|
||||
// A row that already left can still report a layout queued before it went.
|
||||
if (!live.current.has(messageId)) {
|
||||
return
|
||||
}
|
||||
const { showAll } = list
|
||||
shownAll.current = showAll
|
||||
heights.current.set(messageId, height)
|
||||
|
||||
Reference in New Issue
Block a user