198 lines
7.6 KiB
TypeScript
198 lines
7.6 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { useLocalSearchParams, useRouter } from "expo-router";
|
|
import React, { useEffect, useRef, useState } from "react";
|
|
import { ScrollView, Text, View } from "react-native";
|
|
import { useApp } from "../src/app-context";
|
|
import { AppHeader } from "../src/components/AppHeader";
|
|
import { ChatInputBar } from "../src/components/ChatInputBar";
|
|
import { ConsentModal } from "../src/components/ConsentModal";
|
|
import { HanLogo } from "../src/components/HanLogo";
|
|
import { NotificationCarousel } from "../src/components/NotificationCarousel";
|
|
import { PopularQuestionsList } from "../src/components/PopularQuestionsList";
|
|
import { QuickActions } from "../src/components/QuickActions";
|
|
import { ScreenShell } from "../src/components/ScreenShell";
|
|
import {
|
|
clearPendingTextIntent,
|
|
createPendingTextIntent,
|
|
savePendingTextIntent,
|
|
savePendingFileIntent,
|
|
type PendingTextIntent,
|
|
} from "../src/pending-intent";
|
|
import { DEFAULT_MESSAGE_MAX_LENGTH, normalizeMessageText } from "../src/message-text";
|
|
import { publicApi } from "../src/services";
|
|
import type { Consents } from "../src/types";
|
|
import { ErrorNotice, Loading, styles } from "../src/ui";
|
|
|
|
export default function HomeScreen() {
|
|
const { authStatus, authorize } = useApp();
|
|
const config = useQuery({ queryKey: ["public-config"], queryFn: publicApi.config });
|
|
const content = useQuery({ queryKey: ["public-content"], queryFn: publicApi.content });
|
|
const [consentOpen, setConsentOpen] = useState(false);
|
|
const [pending, setPending] = useState<PendingTextIntent | null>(null);
|
|
const [message, setMessage] = useState("");
|
|
const [sendError, setSendError] = useState<unknown>();
|
|
const [sending] = useState(false);
|
|
const [afterNotificationAuth, setAfterNotificationAuth] = useState<(() => Promise<void>) | undefined>();
|
|
const { authorize: authorizeParam } = useLocalSearchParams<{ authorize?: string }>();
|
|
const handledAuthorizeParam = useRef(false);
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
if (authorizeParam === "1" && !handledAuthorizeParam.current && authStatus !== "authenticated") {
|
|
handledAuthorizeParam.current = true;
|
|
setConsentOpen(true);
|
|
}
|
|
}, [authStatus, authorizeParam]);
|
|
|
|
const openChatWithText = (intent: PendingTextIntent) => {
|
|
setSendError(undefined);
|
|
savePendingTextIntent(intent);
|
|
setMessage("");
|
|
setPending(null);
|
|
router.push("/dialogs?pending=1");
|
|
};
|
|
|
|
const send = async (text: string) => {
|
|
const normalized = normalizeMessageText(text);
|
|
if (!normalized) return;
|
|
const maxLength = config.data?.messages?.max_text_length ?? DEFAULT_MESSAGE_MAX_LENGTH;
|
|
if (normalized.length > maxLength) {
|
|
setSendError(new Error(`Сообщение слишком длинное. Максимум — ${maxLength} символов.`));
|
|
return;
|
|
}
|
|
const intent = createPendingTextIntent(normalized);
|
|
if (authStatus !== "authenticated") {
|
|
setPending(intent);
|
|
savePendingTextIntent(intent);
|
|
setConsentOpen(true);
|
|
return;
|
|
}
|
|
openChatWithText(intent);
|
|
};
|
|
|
|
const chooseFile = () => {
|
|
if (authStatus !== "authenticated") {
|
|
setConsentOpen(true);
|
|
return;
|
|
}
|
|
if (typeof document === "undefined") {
|
|
setSendError(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)) {
|
|
setSendError(new Error("Недопустимый тип файла или превышен допустимый размер."));
|
|
return;
|
|
}
|
|
setSendError(undefined);
|
|
savePendingFileIntent({
|
|
file,
|
|
dialogKey: crypto.randomUUID(),
|
|
messageKey: crypto.randomUUID(),
|
|
});
|
|
router.push("/dialogs?pending=1");
|
|
};
|
|
|
|
const accept = async (accepted: {
|
|
personal_data: boolean;
|
|
user_agreement: boolean;
|
|
marketing: boolean;
|
|
}) => {
|
|
const versions = config.data?.consents;
|
|
const consents: Consents = {
|
|
personal_data: { accepted: accepted.personal_data, version: versions?.personal_data?.version ?? "current" },
|
|
user_agreement: { accepted: accepted.user_agreement, version: versions?.user_agreement?.version ?? "current" },
|
|
marketing: { accepted: accepted.marketing, version: versions?.marketing?.version ?? "current" },
|
|
};
|
|
setConsentOpen(false);
|
|
try {
|
|
const authorized = await authorize(consents);
|
|
if (authorized && pending) openChatWithText(pending);
|
|
else if (authorized && afterNotificationAuth) await afterNotificationAuth();
|
|
} catch (error) {
|
|
setSendError(error);
|
|
} finally {
|
|
setAfterNotificationAuth(undefined);
|
|
}
|
|
};
|
|
|
|
const welcome = content.data?.texts.welcome;
|
|
const questions = content.data?.popular_questions ?? [];
|
|
|
|
return (
|
|
<ScreenShell>
|
|
<AppHeader guestLabel={authStatus === "authenticated" ? undefined : "Гость"} />
|
|
<View style={{ flex: 1 }}>
|
|
<HanLogo />
|
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: spacing }}>
|
|
{(config.isLoading || content.isLoading) && <Loading />}
|
|
{(config.error || content.error) && (
|
|
<View style={{ paddingHorizontal: 16 }}>
|
|
<ErrorNotice error={config.error ?? content.error} retry={() => { void config.refetch(); void content.refetch(); }} />
|
|
</View>
|
|
)}
|
|
{welcome ? (
|
|
<Text style={[styles.muted, { paddingHorizontal: 16, marginBottom: 8 }]}>{welcome}</Text>
|
|
) : null}
|
|
<NotificationCarousel
|
|
authenticated={authStatus === "authenticated"}
|
|
autoplay={config.data?.notification?.carousel_autoplay_enabled ?? false}
|
|
autoplayIntervalMs={config.data?.notification?.carousel_autoplay_interval_ms ?? 5000}
|
|
requireAuth={(afterAuth) => {
|
|
setAfterNotificationAuth(afterAuth ? () => afterAuth : undefined);
|
|
setConsentOpen(true);
|
|
}}
|
|
/>
|
|
</ScrollView>
|
|
<PopularQuestionsList questions={questions} onSelect={(text) => { setMessage(text); void send(text); }} />
|
|
<ChatInputBar
|
|
disabled={sending}
|
|
onAttach={chooseFile}
|
|
onChangeText={setMessage}
|
|
onSubmit={() => void send(message)}
|
|
sending={sending}
|
|
value={message}
|
|
maxLength={config.data?.messages?.max_text_length ?? DEFAULT_MESSAGE_MAX_LENGTH}
|
|
/>
|
|
<QuickActions
|
|
authenticated={authStatus === "authenticated"}
|
|
phone={config.data?.operator.call_phone}
|
|
/>
|
|
{Boolean(sendError) && (
|
|
<View style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
|
|
<ErrorNotice error={sendError} />
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{consentOpen && (
|
|
<ConsentModal
|
|
consents={config.data?.consents}
|
|
onAccept={(accepted) => void accept(accepted)}
|
|
onCancel={() => {
|
|
setConsentOpen(false);
|
|
setPending(null);
|
|
setAfterNotificationAuth(undefined);
|
|
clearPendingTextIntent();
|
|
}}
|
|
/>
|
|
)}
|
|
</ScreenShell>
|
|
);
|
|
}
|
|
|
|
const spacing = 16;
|