diff --git a/mobile/src/session/MobileNativeChatQueuedMessages.tsx b/mobile/src/session/MobileNativeChatQueuedMessages.tsx index 15196a3198b..64a8a88b01a 100644 --- a/mobile/src/session/MobileNativeChatQueuedMessages.tsx +++ b/mobile/src/session/MobileNativeChatQueuedMessages.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from 'react' -import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native' +import { Pressable, StyleSheet, Text, View } from 'react-native' import { AlertCircle, ChevronDown, @@ -17,6 +17,7 @@ import { colors, radii, spacing, typography } from '../theme/mobile-theme' import type { MobileQueuedMessageCard } from './mobile-structured-queued-message-cards' import type { MobileQueuePause } from './mobile-structured-queued-message-feed' import type { MobileQueuedMessageEdit } from './use-mobile-structured-queued-message-controls' +import { MobileQueuedCardText } from './MobileQueuedCardText' /** The header's in-flight keys beside the cards' message ids, which never contain NUL. */ const RESUME_KEY = '\u0000resume' @@ -99,6 +100,13 @@ export function MobileNativeChatQueuedMessages({ {pause ? 'Paused' : 'Queued'} + + · + {attentionCaption ? `${cards.length} · ${attentionCaption}` : cards.length} @@ -273,61 +281,6 @@ export function MobileNativeChatQueuedMessages({ ) } -/** Two lines, opening on tap when it clips: a card can hold another agent's message the person - * never read, and Steer or Delete must not be a blind choice. Opened, a long one scrolls in a - * capped box. */ -function MobileQueuedCardText({ text }: { text: string }): React.JSX.Element { - const [expanded, setExpanded] = useState(false) - const [clipped, setClipped] = useState(false) - // The whole text laid out unseen at the card's width: more than two lines is what clips. - const measure = ( - setClipped(event.nativeEvent.lines.length > 2)} - > - {text} - - ) - if (!clipped && !expanded) { - return ( - - {measure} - - {text} - - - ) - } - const toggle = ( - setExpanded(!expanded)} - > - - {text} - - {expanded ? 'Show less' : 'Show more'} - - ) - // The press sits inside the scroll, so a drag scrolls and only a tap folds it. - return expanded ? ( - - {toggle} - - ) : ( - - {measure} - {toggle} - - ) -} - // Every action touches as a 44pt target (platform floor) inside its row: Android drops touches // outside the parent, so the row is at least that tall and nothing overhangs it. const MIN_TOUCH_TARGET = 44 @@ -378,19 +331,6 @@ const styles = StyleSheet.create({ paddingVertical: spacing.sm, gap: 2 }, - body: { - color: colors.textPrimary, - fontSize: typography.bodySize - }, - expandedBody: { - maxHeight: 240 - }, - measure: { - position: 'absolute', - left: 0, - right: 0, - opacity: 0 - }, caption: { color: colors.textMuted, fontSize: typography.metaSize diff --git a/mobile/src/session/MobileQueuedCardText.tsx b/mobile/src/session/MobileQueuedCardText.tsx new file mode 100644 index 00000000000..fe690bcad67 --- /dev/null +++ b/mobile/src/session/MobileQueuedCardText.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react' +import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native' +import { colors, typography } from '../theme/mobile-theme' + +/** Two lines, opening on tap when it clips: a card can hold another agent's message the person + * never read, and Steer or Delete must not be a blind choice. Opened, a long one scrolls in a + * capped box. */ +export function MobileQueuedCardText({ text }: { text: string }): React.JSX.Element { + const [expanded, setExpanded] = useState(false) + const [clipped, setClipped] = useState(false) + // The whole text laid out unseen at the card's width: more than two lines is what clips. + const measure = ( + setClipped(event.nativeEvent.lines.length > 2)} + > + {text} + + ) + if (!clipped && !expanded) { + return ( + + {measure} + + {text} + + + ) + } + const toggle = ( + setExpanded(!expanded)} + > + + {text} + + {expanded ? 'Show less' : 'Show more'} + + ) + // The press sits inside the scroll, so a drag scrolls and only a tap folds it. + return expanded ? ( + + {toggle} + + ) : ( + + {measure} + {toggle} + + ) +} + +const styles = StyleSheet.create({ + body: { + color: colors.textPrimary, + fontSize: typography.bodySize + }, + expandedBody: { + maxHeight: 240 + }, + measure: { + position: 'absolute', + left: 0, + right: 0, + opacity: 0 + }, + caption: { + color: colors.textMuted, + fontSize: typography.metaSize + } +}) diff --git a/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx b/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx index d579520efb7..91044ff3b54 100644 --- a/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx @@ -504,7 +504,7 @@ describe('NativeChatQueuedMessageList', () => { it('a paused queue holds every card, in order, under one header with Clear all and Resume', () => { renderHeldQueue([waitingDraft('a', 1), waitingDraft('b', 2)], { reason: 'stopped' }) const header = screen.getByRole('list').previousElementSibling! - expect(header.textContent).toBe('Paused2Clear allResume') + expect(header.textContent).toBe('Paused·2Clear allResume') expect(screen.getAllByRole('button', { name: 'Steer' })).toHaveLength(2) }) @@ -518,7 +518,7 @@ describe('NativeChatQueuedMessageList', () => { it('a queue that drains on its own (or after a restart) reads Queued, with no Resume; no cards, nothing', () => { renderHeldQueue([waitingDraft('waiting', 1)], null) - expect(screen.getByRole('list').previousElementSibling?.textContent).toBe('Queued1') + expect(screen.getByRole('list').previousElementSibling?.textContent).toBe('Queued·1') expect(screen.queryByRole('button', { name: 'Resume' })).toBeNull() expect(screen.queryByRole('button', { name: 'Clear all' })).toBeNull() cleanup() @@ -538,7 +538,7 @@ describe('NativeChatQueuedMessageList', () => { stopped ) expect(screen.queryByRole('button', { name: 'Resume' })).toBeNull() - expect(container.textContent).toContain('Queued3') + expect(container.textContent).toContain('Queued·3') }) it('Resume shows it is pending and hands focus back', async () => { @@ -622,7 +622,7 @@ describe('NativeChatQueuedMessageList', () => { ) // The header is the box's first row, outside the list; the list is its second. expect(box.children).toHaveLength(2) - expect(box.firstElementChild?.textContent).toContain('Paused3') + expect(box.firstElementChild?.textContent).toContain('Paused·3') expect(box.lastElementChild).toBe(list) expect(list.className.split(' ')).toContain('divide-y') const rows = within(list).getAllByRole('listitem') diff --git a/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx index d19cbfa8363..42f2b658082 100644 --- a/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx +++ b/src/renderer/src/components/native-chat/NativeChatQueuedMessageList.tsx @@ -198,6 +198,7 @@ function NativeChatQueueHeader({ ? translate('components.native-chat.queuedMessages.paused', 'Paused') : translate('components.native-chat.queuedMessages.queued', 'Queued')}

+ · {count}