Files
orca/mobile/src/session/MobileNativeChatQueuedMessageEditor.tsx
T
Brennan Benson 285f95c045 fix(mobile): a compact phone queue: shorter header, a capped scrolling list, and an editor that fits the card (#30016)
* fix(mobile): a compact queue: shorter header, a capped scrolling list, and an editor that fits the card

* fix(mobile): keep queue actions visible and reveal new cards

* test(mobile): allow shared SVG host fixture

* fix(mobile): queue header and editor controls grow with large system text instead of clipping

* fix(mobile): at large text sizes the queue header's buttons wrap below its label instead of squeezing it out

* fix(mobile): at large text sizes the queued editor's Cancel and Save wrap below its heading instead of truncating it

* fix(mobile): at large text sizes the queue header's and editor's buttons take a line of their own

* fix(mobile): the large-text button lines wrap when one button is all a small phone's line holds

* test(mobile): the composer tray test's react-native stub reports the text scale

* fix(mobile): while a card is edited at large text, the paused header's button line gives its height to the editor
2026-10-17 13:47:48 -07:00

172 lines
5.2 KiB
TypeScript

import { useEffect, useRef } from 'react'
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'
import { Pencil } from 'lucide-react-native'
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
import { useLargeSystemText } from '../theme/use-large-system-text'
import { TEXT_INPUT_FONT_SIZE } from '../platform/text-input-font-size'
import type { MobileQueuedMessageInlineEditor } from './use-mobile-structured-queued-edit'
const PENCIL_SIZE = 14
// The composer's own button height.
const EDITOR_ACTION_HEIGHT = 40
/** The editor that replaces a queued card's row: one action line, then the text on the card. Save
* sits on that line rather than under the field, as the keyboard leaves little height. */
export function MobileNativeChatQueuedMessageEditor({
editor,
attribution
}: {
editor: MobileQueuedMessageInlineEditor
/** The card's "From <name>" line, kept above the text. */
attribution: string | null
}): React.JSX.Element {
const input = useRef<TextInput>(null)
useEffect(() => {
input.current?.focus()
}, [])
const saveDisabled = editor.saving || !editor.canSave
const largeText = useLargeSystemText()
const actions = (
<>
<Pressable
accessibilityRole="button"
accessibilityLabel="Cancel editing queued message"
accessibilityState={{ disabled: editor.saving }}
disabled={editor.saving}
style={({ pressed }) => [
styles.cancel,
pressed && styles.pressed,
editor.saving && styles.disabled
]}
onPress={editor.cancel}
>
<Text style={styles.cancelLabel}>Cancel</Text>
</Pressable>
{/* A 40pt target around a smaller pill, so the line stays one button tall. */}
<Pressable
accessibilityRole="button"
accessibilityLabel="Save queued message"
accessibilityState={{ disabled: saveDisabled }}
disabled={saveDisabled}
style={[styles.save, saveDisabled && styles.disabled]}
onPress={() => void editor.save()}
>
{({ pressed }) => (
<View style={[styles.savePill, pressed && styles.savePillPressed]}>
<Text style={styles.saveLabel}>Save</Text>
</View>
)}
</Pressable>
</>
)
return (
<View style={styles.editor} testID="queued-card-editor">
<View style={styles.head}>
<Pencil size={PENCIL_SIZE} color={colors.textMuted} strokeWidth={2} />
<Text style={styles.label}>Editing message</Text>
{largeText ? null : actions}
</View>
{largeText ? <View style={styles.actionsLine}>{actions}</View> : null}
{attribution ? (
<Text style={styles.attribution} numberOfLines={1}>
{attribution}
</Text>
) : null}
<TextInput
ref={input}
accessibilityLabel="Edit queued message"
value={editor.text}
editable={!editor.saving}
multiline
selectionColor={colors.accentBlue}
style={styles.input}
onChangeText={editor.change}
/>
</View>
)
}
// The text column under the pencil, where the card's text was.
const TEXT_INDENT = PENCIL_SIZE + spacing.sm
const styles = StyleSheet.create({
editor: {
backgroundColor: colors.bgRaised,
paddingLeft: spacing.md,
paddingRight: spacing.xs,
paddingBottom: spacing.sm
},
head: {
minHeight: EDITOR_ACTION_HEIGHT,
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm
},
// Large text: Cancel and Save on a line of their own, under the heading.
actionsLine: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-end',
gap: spacing.sm
},
label: {
flex: 1,
color: colors.textMuted,
fontSize: typography.metaSize,
lineHeight: typography.metaSize + spacing.xs
},
// minHeight, not height: the action line grows with the system text size instead of clipping.
cancel: {
minHeight: EDITOR_ACTION_HEIGHT,
minWidth: 56,
paddingHorizontal: spacing.sm,
alignItems: 'center',
justifyContent: 'center',
borderRadius: radii.button
},
cancelLabel: {
color: colors.textSecondary,
fontSize: typography.metaSize,
fontWeight: '500'
},
save: {
minHeight: EDITOR_ACTION_HEIGHT,
justifyContent: 'center'
},
savePill: {
minHeight: spacing.xl + spacing.xs,
paddingHorizontal: spacing.md,
borderRadius: radii.button,
backgroundColor: colors.textPrimary,
alignItems: 'center',
justifyContent: 'center'
},
savePillPressed: {
backgroundColor: colors.textSecondary
},
saveLabel: {
color: colors.bgBase,
fontSize: typography.metaSize,
fontWeight: '600'
},
attribution: {
marginLeft: TEXT_INDENT,
color: colors.textMuted,
fontSize: typography.metaSize,
lineHeight: typography.metaSize + spacing.xs
},
// On the card itself: no fill or border, and no padding above or below the text.
input: {
marginLeft: TEXT_INDENT,
padding: 0,
paddingRight: spacing.md,
maxHeight: 80,
color: colors.textPrimary,
fontSize: TEXT_INPUT_FONT_SIZE,
lineHeight: 20,
textAlignVertical: 'top'
},
pressed: { backgroundColor: colors.bgPanel },
disabled: { opacity: 0.5 }
})