Files
orca/mobile/src/session/MobileNativeChatMessageActionsSheet.tsx
T
Kelvin Amoaba 6d519f7835 fix(mobile): keep content clear of the side insets in landscape (#28749)
In landscape the iPhone sensor housing and Android's side nav bar are left/right
insets, which no screen or over-the-air page padded for.

Fixes #21190
2026-10-15 00:57:56 +00:00

130 lines
4.1 KiB
TypeScript

import { useRef, useState } from 'react'
import { Alert, Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
import { Copy, TextSelect, X } from 'lucide-react-native'
import type { NativeChatMessage } from '../../../src/shared/native-chat-types'
import { ActionSheetContent } from '../components/ActionSheetModal'
import { BottomDrawer } from '../components/BottomDrawer'
import { useClipboardWriter } from '../platform/clipboard'
import { useWindowSafeAreaInsets } from '../layout/side-safe-area'
import { colors, spacing, typography } from '../theme/mobile-theme'
import { nativeChatMessagePlainText } from './mobile-native-chat-message-plain-text'
type Props = {
/** The long-pressed message. The owner mounts this only while the sheet is open. */
message: NativeChatMessage
onClose: () => void
}
export function MobileNativeChatMessageActionsSheet({
message,
onClose
}: Props): React.JSX.Element {
const clipboard = useClipboardWriter()
const [sheetVisible, setSheetVisible] = useState(true)
const [selecting, setSelecting] = useState(false)
// Wait for the drawer to unmount before presenting another native modal.
const selectRequested = useRef(false)
// Streaming updates must not reset an active native text selection.
const [text] = useState(() => nativeChatMessagePlainText(message))
const closeSheet = () => setSheetVisible(false)
return (
<>
<BottomDrawer
visible={sheetVisible}
onClose={closeSheet}
onAfterClose={() => (selectRequested.current ? setSelecting(true) : onClose())}
dragContentToDismiss
>
<ActionSheetContent
onClose={closeSheet}
actions={[
{
label: 'Copy message',
icon: Copy,
disabled: text.length === 0,
onPress: () => {
void clipboard.writeText(text).catch((error: unknown) => {
Alert.alert(
'Copy failed',
error instanceof Error ? error.message : 'The clipboard rejected the text.'
)
})
}
},
{
label: 'Select text',
icon: TextSelect,
disabled: text.length === 0,
onPress: () => {
selectRequested.current = true
}
}
]}
/>
</BottomDrawer>
{selecting ? <SelectTextScreen text={text} onClose={onClose} /> : null}
</>
)
}
function SelectTextScreen({
text,
onClose
}: {
text: string
onClose: () => void
}): React.JSX.Element {
const insets = useWindowSafeAreaInsets()
return (
<Modal visible animationType="slide" onRequestClose={onClose}>
<View
style={[
styles.screen,
{
paddingTop: insets.top,
paddingRight: insets.right,
paddingBottom: insets.bottom,
paddingLeft: insets.left
}
]}
>
<View style={styles.header}>
<Text style={styles.title}>Select text</Text>
<Pressable
onPress={onClose}
hitSlop={12}
accessibilityLabel="Close"
accessibilityRole="button"
style={styles.close}
>
<X size={20} color={colors.textSecondary} />
</Pressable>
</View>
<ScrollView contentContainerStyle={styles.body}>
<Text selectable style={styles.text}>
{text}
</Text>
</ScrollView>
</View>
</Modal>
)
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: colors.bgBase },
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.borderSubtle
},
title: { color: colors.textPrimary, fontSize: typography.titleSize, fontWeight: '600' },
close: { padding: spacing.xs },
body: { padding: spacing.lg },
text: { color: colors.textPrimary, fontSize: typography.bodySize, lineHeight: 22 }
})