|
| 1 | +import React, { useEffect, useRef } from "react"; |
| 2 | +import { |
| 3 | + KeyboardAvoidingView, |
| 4 | + Modal, |
| 5 | + Platform, |
| 6 | + ScrollView, |
| 7 | + TextInput, |
| 8 | + TouchableOpacity, |
| 9 | + View, |
| 10 | +} from "react-native"; |
| 11 | +import { Ionicons } from "@expo/vector-icons"; |
| 12 | +import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 13 | +import { useTheme } from "../theme"; |
| 14 | +import { ThemedText } from "./ThemedText"; |
| 15 | + |
| 16 | +type FullscreenComposerModalProps = { |
| 17 | + visible: boolean; |
| 18 | + value: string; |
| 19 | + placeholder: string; |
| 20 | + isEditing: boolean; |
| 21 | + isSending: boolean; |
| 22 | + onChangeText: (text: string) => void; |
| 23 | + onClose: () => void; |
| 24 | + onSend: () => Promise<boolean> | boolean; |
| 25 | +}; |
| 26 | + |
| 27 | +export function FullscreenComposerModal(props: FullscreenComposerModalProps) { |
| 28 | + const { visible, value, placeholder, isEditing, isSending, onChangeText, onClose, onSend } = |
| 29 | + props; |
| 30 | + const theme = useTheme(); |
| 31 | + const spacing = theme.spacing; |
| 32 | + const insets = useSafeAreaInsets(); |
| 33 | + const inputRef = useRef<TextInput>(null); |
| 34 | + |
| 35 | + useEffect(() => { |
| 36 | + if (visible) { |
| 37 | + const timeout = setTimeout(() => { |
| 38 | + inputRef.current?.focus(); |
| 39 | + }, 150); |
| 40 | + return () => clearTimeout(timeout); |
| 41 | + } |
| 42 | + return undefined; |
| 43 | + }, [visible]); |
| 44 | + |
| 45 | + const disabled = isSending || value.trim().length === 0; |
| 46 | + |
| 47 | + return ( |
| 48 | + <Modal |
| 49 | + animationType="slide" |
| 50 | + presentationStyle="fullScreen" |
| 51 | + visible={visible} |
| 52 | + onRequestClose={onClose} |
| 53 | + statusBarTranslucent |
| 54 | + > |
| 55 | + <KeyboardAvoidingView |
| 56 | + style={{ flex: 1 }} |
| 57 | + behavior={Platform.OS === "ios" ? "padding" : undefined} |
| 58 | + keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 24} |
| 59 | + > |
| 60 | + <View |
| 61 | + style={{ |
| 62 | + flex: 1, |
| 63 | + backgroundColor: theme.colors.surface, |
| 64 | + paddingTop: Math.max(insets.top, spacing.lg), |
| 65 | + paddingBottom: Math.max(insets.bottom, spacing.lg), |
| 66 | + }} |
| 67 | + > |
| 68 | + <View |
| 69 | + style={{ |
| 70 | + flexDirection: "row", |
| 71 | + alignItems: "center", |
| 72 | + justifyContent: "space-between", |
| 73 | + paddingHorizontal: spacing.lg, |
| 74 | + paddingBottom: spacing.md, |
| 75 | + borderBottomWidth: 1, |
| 76 | + borderBottomColor: theme.colors.border, |
| 77 | + }} |
| 78 | + > |
| 79 | + <TouchableOpacity |
| 80 | + onPress={onClose} |
| 81 | + accessibilityRole="button" |
| 82 | + accessibilityLabel="Close full composer" |
| 83 | + style={{ |
| 84 | + padding: spacing.sm, |
| 85 | + borderRadius: spacing.sm, |
| 86 | + backgroundColor: theme.colors.surfaceSecondary, |
| 87 | + }} |
| 88 | + > |
| 89 | + <Ionicons name="close" size={20} color={theme.colors.foregroundPrimary} /> |
| 90 | + </TouchableOpacity> |
| 91 | + |
| 92 | + <ThemedText weight="semibold" style={{ color: theme.colors.foregroundPrimary }}> |
| 93 | + {isEditing ? "Edit message" : "Full composer"} |
| 94 | + </ThemedText> |
| 95 | + |
| 96 | + <TouchableOpacity |
| 97 | + onPress={() => { |
| 98 | + const result = onSend(); |
| 99 | + if (result && typeof (result as Promise<boolean>).then === "function") { |
| 100 | + void (result as Promise<boolean>); |
| 101 | + } |
| 102 | + }} |
| 103 | + disabled={disabled} |
| 104 | + accessibilityRole="button" |
| 105 | + accessibilityLabel="Send message" |
| 106 | + style={{ |
| 107 | + paddingVertical: spacing.sm, |
| 108 | + paddingHorizontal: spacing.lg, |
| 109 | + borderRadius: theme.radii.sm, |
| 110 | + backgroundColor: disabled ? theme.colors.border : theme.colors.accent, |
| 111 | + opacity: disabled ? 0.6 : 1, |
| 112 | + }} |
| 113 | + > |
| 114 | + <ThemedText |
| 115 | + weight="semibold" |
| 116 | + style={{ color: disabled ? theme.colors.foregroundMuted : "#fff" }} |
| 117 | + > |
| 118 | + {isSending ? "Sending…" : isEditing ? "Save" : "Send"} |
| 119 | + </ThemedText> |
| 120 | + </TouchableOpacity> |
| 121 | + </View> |
| 122 | + |
| 123 | + {isEditing && ( |
| 124 | + <View |
| 125 | + style={{ |
| 126 | + paddingHorizontal: spacing.lg, |
| 127 | + paddingVertical: spacing.sm, |
| 128 | + backgroundColor: theme.colors.accentMuted, |
| 129 | + borderBottomWidth: 1, |
| 130 | + borderBottomColor: theme.colors.border, |
| 131 | + }} |
| 132 | + > |
| 133 | + <ThemedText style={{ color: theme.colors.accent }}> |
| 134 | + Editing existing message |
| 135 | + </ThemedText> |
| 136 | + </View> |
| 137 | + )} |
| 138 | + |
| 139 | + <ScrollView |
| 140 | + contentContainerStyle={{ flexGrow: 1, padding: spacing.lg }} |
| 141 | + keyboardShouldPersistTaps="handled" |
| 142 | + > |
| 143 | + <View |
| 144 | + style={{ |
| 145 | + flex: 1, |
| 146 | + borderWidth: 1, |
| 147 | + borderColor: theme.colors.inputBorder, |
| 148 | + borderRadius: theme.radii.md, |
| 149 | + backgroundColor: theme.colors.inputBackground, |
| 150 | + padding: spacing.md, |
| 151 | + minHeight: 200, |
| 152 | + }} |
| 153 | + > |
| 154 | + <TextInput |
| 155 | + ref={inputRef} |
| 156 | + value={value} |
| 157 | + onChangeText={onChangeText} |
| 158 | + placeholder={placeholder} |
| 159 | + placeholderTextColor={theme.colors.foregroundMuted} |
| 160 | + style={{ |
| 161 | + color: theme.colors.foregroundPrimary, |
| 162 | + fontSize: 16, |
| 163 | + flex: 1, |
| 164 | + textAlignVertical: "top", |
| 165 | + }} |
| 166 | + multiline |
| 167 | + autoCorrect={false} |
| 168 | + autoCapitalize="sentences" |
| 169 | + autoFocus={visible} |
| 170 | + /> |
| 171 | + </View> |
| 172 | + |
| 173 | + <ThemedText |
| 174 | + variant="caption" |
| 175 | + style={{ |
| 176 | + marginTop: spacing.md, |
| 177 | + color: theme.colors.foregroundMuted, |
| 178 | + textAlign: "center", |
| 179 | + }} |
| 180 | + > |
| 181 | + Draft comfortably here, then tap Send when you are ready. |
| 182 | + </ThemedText> |
| 183 | + </ScrollView> |
| 184 | + </View> |
| 185 | + </KeyboardAvoidingView> |
| 186 | + </Modal> |
| 187 | + ); |
| 188 | +} |
0 commit comments