mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 10:37:35 +00:00
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
130 lines
4.1 KiB
TypeScript
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 }
|
|
})
|