/**
* AttachmentMenu — bottom-sheet с вариантами прикрепления.
*
* Выводится при нажатии на `+` в composer'е. Опции:
* - 📷 Photo / video из галереи (expo-image-picker)
* - 📸 Take photo (камера)
* - 📎 File (expo-document-picker)
* - 🎙️ Voice message — stub (запись через expo-av потребует
* permissions runtime + recording UI; сейчас добавляет мок-
* голосовое с duration 4s)
*
* Всё визуально — тёмный overlay + sheet снизу. Закрытие по tap'у на
* overlay или на Cancel.
*/
import React from 'react';
import { View, Text, Pressable, Alert, Modal } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as ImagePicker from 'expo-image-picker';
import * as DocumentPicker from 'expo-document-picker';
import type { Attachment } from '@/lib/types';
export interface AttachmentMenuProps {
visible: boolean;
onClose: () => void;
/** Вызывается когда attachment готов для отправки. */
onPick: (att: Attachment) => void;
}
export function AttachmentMenu({ visible, onClose, onPick }: AttachmentMenuProps) {
const insets = useSafeAreaInsets();
const pickImageOrVideo = async () => {
try {
const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!perm.granted) {
Alert.alert('Permission needed', 'Grant photos access to attach media.');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
quality: 0.85,
allowsEditing: false,
});
if (result.canceled) return;
const asset = result.assets[0];
onPick({
kind: asset.type === 'video' ? 'video' : 'image',
uri: asset.uri,
mime: asset.mimeType,
width: asset.width,
height: asset.height,
duration: asset.duration ? Math.round(asset.duration / 1000) : undefined,
});
onClose();
} catch (e: any) {
Alert.alert('Pick failed', e?.message ?? 'Unknown error');
}
};
const takePhoto = async () => {
try {
const perm = await ImagePicker.requestCameraPermissionsAsync();
if (!perm.granted) {
Alert.alert('Permission needed', 'Grant camera access to take a photo.');
return;
}
const result = await ImagePicker.launchCameraAsync({ quality: 0.85 });
if (result.canceled) return;
const asset = result.assets[0];
onPick({
kind: asset.type === 'video' ? 'video' : 'image',
uri: asset.uri,
mime: asset.mimeType,
width: asset.width,
height: asset.height,
});
onClose();
} catch (e: any) {
Alert.alert('Camera failed', e?.message ?? 'Unknown error');
}
};
const pickFile = async () => {
try {
const res = await DocumentPicker.getDocumentAsync({
type: '*/*',
copyToCacheDirectory: true,
});
if (res.canceled) return;
const asset = res.assets[0];
onPick({
kind: 'file',
uri: asset.uri,
name: asset.name,
mime: asset.mimeType ?? undefined,
size: asset.size,
});
onClose();
} catch (e: any) {
Alert.alert('File pick failed', e?.message ?? 'Unknown error');
}
};
// Voice recorder больше не stub — см. inline-кнопку 🎤 в composer'е,
// которая разворачивает VoiceRecorder (expo-av Audio.Recording). Опция
// Voice в этом меню убрана, т.к. дублировала бы UX.
return (
{}}
style={{
backgroundColor: '#111111',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingTop: 8,
paddingBottom: Math.max(insets.bottom, 12) + 10,
paddingHorizontal: 10,
borderTopWidth: 1, borderColor: '#1f1f1f',
}}
>
{/* Drag handle */}
Attach
);
}
function Row({
icon, label, onPress,
}: {
icon: React.ComponentProps['name'];
label: string;
onPress: () => void;
}) {
return (
({
flexDirection: 'row',
alignItems: 'center',
gap: 14,
paddingHorizontal: 14,
paddingVertical: 14,
borderRadius: 14,
backgroundColor: pressed ? '#1a1a1a' : 'transparent',
})}
>
{label}
);
}