import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useLocalSearchParams, useRouter } from "expo-router"; import React, { useEffect, useMemo, useState } from "react"; import { Platform, ScrollView, Text, View } from "react-native"; import { useApp } from "../../src/app-context"; import { RealtimeClient, reconcileMessages } from "../../src/realtime"; import { dialogApi, profileApi, publicApi, uploadAttachment } from "../../src/services"; import type { Message } from "../../src/types"; import { Button, ErrorNotice, Field, Header, Loading, styles } from "../../src/ui"; const statusLabel: Record = { open: "Открыт", waiting_for_company: "Ожидает ответа компании", waiting_for_client: "Ожидает вашего ответа", closed: "Закрыт", accepted: "Принято", delivered: "Доставлено", failed: "Ошибка доставки", rejected: "Отклонено", }; export default function ChatScreen() { const { dialogId } = useLocalSearchParams<{ dialogId: string }>(); const app = useApp(); const client = useQueryClient(); const router = useRouter(); const [text, setText] = useState(""); const [error, setError] = useState(); const [sending, setSending] = useState(false); const [creating, setCreating] = useState(false); const config = useQuery({ queryKey: ["public-config"], queryFn: publicApi.config }); const dialog = useQuery({ queryKey: ["dialog", dialogId], queryFn: () => dialogApi.get(dialogId), enabled: Boolean(dialogId) && app.authStatus === "authenticated" }); const messages = useQuery({ queryKey: ["messages", dialogId], queryFn: () => dialogApi.messages(dialogId), enabled: Boolean(dialogId) && app.authStatus === "authenticated" }); const realtime = useMemo(() => new RealtimeClient( dialogId ? [dialogId] : [], (event) => { if (event.type === "message.new") merge([event.message]); if (event.type === "message.status") { client.setQueryData(["messages", dialogId], (old: typeof messages.data) => old && ({ ...old, items: old.items.map((item) => item.message_id === event.message_id ? { ...item, safety_status: event.safety_status, delivery_status: event.delivery_status } : item), })); } if (event.type === "dialog.status") void dialog.refetch(); }, (_, incoming) => merge(incoming), app.setRealtimeState, ), [dialogId]); function merge(incoming: Message[]) { client.setQueryData(["messages", dialogId], (old: typeof messages.data) => old ? { ...old, items: reconcileMessages(old.items, incoming) } : { items: incoming, next_cursor: null }, ); } useEffect(() => { if (app.authStatus === "authenticated") realtime.start(); return () => realtime.stop(); }, [realtime, app.authStatus]); const sendText = async () => { const normalized = text.trim(); if (!normalized || !dialogId) return; setSending(true); setError(undefined); try { const message = await dialogApi.sendText(dialogId, normalized, crypto.randomUUID()); merge([message]); setText(""); } catch (reason) { setError(reason); } finally { setSending(false); } }; const chooseFile = () => { if (Platform.OS !== "web") { setError(new Error("Выбор файла в этой тестовой сборке доступен в web.")); return; } const input = document.createElement("input"); input.type = "file"; input.accept = "image/*,application/pdf"; input.onchange = () => { const file = input.files?.[0]; if (file) void sendFile(file); }; input.click(); }; const sendFile = async (file: File) => { const limits = config.data?.attachments; const max = (limits?.max_size_mb ?? 5) * 1024 * 1024; const allowed = limits?.allowed_mime_types ?? ["image/jpeg", "image/png", "image/webp", "application/pdf"]; if (file.size > max || !allowed.includes(file.type)) { setError(new Error("Недопустимый тип файла или превышен допустимый размер.")); return; } setSending(true); setError(undefined); try { const uploaded = await uploadAttachment(dialogId, file); const message = await dialogApi.sendFile(dialogId, uploaded.attachmentId, uploaded.checksum, crypto.randomUUID()); merge([message]); } catch (reason) { setError(reason); } finally { setSending(false); } }; const downloadAttachment = async (attachmentId: string) => { try { const result = await profileApi.attachmentUrl(dialogId, attachmentId); if (typeof window !== "undefined") window.location.assign(result.download_url); } catch (reason) { setError(reason); } }; const createDialog = async () => { setCreating(true); setError(undefined); try { const created = await dialogApi.create(crypto.randomUUID()); router.replace(`/dialogs/${created.dialog_id}`); } catch (reason) { setError(reason); } finally { setCreating(false); } }; const closed = dialog.data?.status === "closed"; return
void app.signOut() : undefined} /> Чат {app.authStatus !== "authenticated" && Для просмотра чата требуется авторизация.} {(dialog.isLoading || messages.isLoading) && } {(dialog.error || messages.error) && { void dialog.refetch(); void messages.refetch(); }} />} Статус: {statusLabel[dialog.data?.status ?? ""] ?? "—"} {(messages.data?.items ?? []).map((message) => {message.content_kind === "file" ? `Файл: ${message.attachments[0]?.file_name ?? "вложение"}` : message.text} {message.attachments.map((attachment) =>