mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(native-chat): a dot between the queue header's label and its count (#29268)
* fix(native-chat): a dot between the queue header's label and its count * style(mobile): wrap the queue header dot's props * refactor(mobile): move the queued card's text into its own file The queue file sat at the 400-line cap, so the header dot had no room.
This commit is contained in:
@@ -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({
|
||||
<Text style={[styles.headerLabel, pause && styles.headerLabelPaused]}>
|
||||
{pause ? 'Paused' : 'Queued'}
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.headerLabel}
|
||||
importantForAccessibility="no"
|
||||
accessibilityElementsHidden
|
||||
>
|
||||
·
|
||||
</Text>
|
||||
<Text style={headerCountStyle} numberOfLines={1}>
|
||||
{attentionCaption ? `${cards.length} · ${attentionCaption}` : cards.length}
|
||||
</Text>
|
||||
@@ -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 = (
|
||||
<Text
|
||||
style={[styles.body, styles.measure]}
|
||||
pointerEvents="none"
|
||||
accessibilityElementsHidden
|
||||
importantForAccessibility="no-hide-descendants"
|
||||
onTextLayout={(event) => setClipped(event.nativeEvent.lines.length > 2)}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
)
|
||||
if (!clipped && !expanded) {
|
||||
return (
|
||||
<View>
|
||||
{measure}
|
||||
<Text style={styles.body} numberOfLines={2}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
const toggle = (
|
||||
<Pressable
|
||||
testID="queued-card-text"
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ expanded }}
|
||||
accessibilityHint={expanded ? 'Shows less of the message' : 'Shows the whole message'}
|
||||
onPress={() => setExpanded(!expanded)}
|
||||
>
|
||||
<Text style={styles.body} numberOfLines={expanded ? undefined : 2}>
|
||||
{text}
|
||||
</Text>
|
||||
<Text style={styles.caption}>{expanded ? 'Show less' : 'Show more'}</Text>
|
||||
</Pressable>
|
||||
)
|
||||
// The press sits inside the scroll, so a drag scrolls and only a tap folds it.
|
||||
return expanded ? (
|
||||
<ScrollView style={styles.expandedBody} nestedScrollEnabled>
|
||||
{toggle}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View>
|
||||
{measure}
|
||||
{toggle}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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 = (
|
||||
<Text
|
||||
style={[styles.body, styles.measure]}
|
||||
pointerEvents="none"
|
||||
accessibilityElementsHidden
|
||||
importantForAccessibility="no-hide-descendants"
|
||||
onTextLayout={(event) => setClipped(event.nativeEvent.lines.length > 2)}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
)
|
||||
if (!clipped && !expanded) {
|
||||
return (
|
||||
<View>
|
||||
{measure}
|
||||
<Text style={styles.body} numberOfLines={2}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
const toggle = (
|
||||
<Pressable
|
||||
testID="queued-card-text"
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ expanded }}
|
||||
accessibilityHint={expanded ? 'Shows less of the message' : 'Shows the whole message'}
|
||||
onPress={() => setExpanded(!expanded)}
|
||||
>
|
||||
<Text style={styles.body} numberOfLines={expanded ? undefined : 2}>
|
||||
{text}
|
||||
</Text>
|
||||
<Text style={styles.caption}>{expanded ? 'Show less' : 'Show more'}</Text>
|
||||
</Pressable>
|
||||
)
|
||||
// The press sits inside the scroll, so a drag scrolls and only a tap folds it.
|
||||
return expanded ? (
|
||||
<ScrollView style={styles.expandedBody} nestedScrollEnabled>
|
||||
{toggle}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View>
|
||||
{measure}
|
||||
{toggle}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
@@ -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')
|
||||
|
||||
@@ -198,6 +198,7 @@ function NativeChatQueueHeader({
|
||||
? translate('components.native-chat.queuedMessages.paused', 'Paused')
|
||||
: translate('components.native-chat.queuedMessages.queued', 'Queued')}
|
||||
</p>
|
||||
<span aria-hidden>·</span>
|
||||
<span className={cn('tabular-nums', attentionCaption ? 'shrink-0' : 'min-w-0 flex-1')}>
|
||||
{count}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user