import type { EntryKind } from "convex-feedback"; import { useMemo, useState } from "react"; import { Alert, Modal, Platform, Pressable, ScrollView, Text, View, } from "react-native"; import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider"; import { useFeedbackUi } from "../../../shared/context/FeedbackProvider"; import { allowAuthenticatedAction } from "../../../shared/helpers.js"; import type { FeedbackScreenEntryModalProps } from "../../../shared/types"; import { collectEntryMetadata } from "../../../shared/metadata"; import { EntryDetail } from "./EntryDetail"; import { ChoiceChips, FeedbackForm } from "./primitives"; import { useNativeAction } from "../helpers.js"; export function CreateEntryModal({ onRequestClose, onCreated, }: FeedbackScreenEntryModalProps & { onRequestClose: () => void; }) { const { enabledKinds } = useFeedbackBody(); const { messages, theme } = useFeedbackUi(); const isIos = Platform.OS === "ios"; const [previewEntryId, setPreviewEntryId] = useState(null); const [kind, setKind] = useState(enabledKinds[0] ?? "feedback"); const [title, setTitle] = useState(""); const [body, setBody] = useState(""); const modalTile = enabledKinds.length === 1 ? messages.newFeedback[enabledKinds.at(0)!] : messages.board.createEntry; return ( {isIos && ( )} {modalTile} {messages.form.cancel} {previewEntryId !== null ? ( setPreviewEntryId(null)} /> ) : ( )} ); } export function CreateEntryForm({ kind, onKindChange, title, onTitleChange, body, onBodyChange, onCreated, onOpenSuggestion, }: FeedbackScreenEntryModalProps & { kind: EntryKind; onKindChange: (kind: EntryKind) => void; title: string; onTitleChange: (title: string) => void; body: string; onBodyChange: (body: string) => void; onOpenSuggestion: (id: string) => void; }) { const { hooks, enabledKinds, collectMetadata, collectStandardMetadata, isAuthenticated, onUnauthenticated, } = useFeedbackBody(); const { messages, theme } = useFeedbackUi(); const create = hooks.useCreateEntry(); const action = useNativeAction(); const similar = hooks.useSimilarEntries({ title, body, kind, limit: 3 }); const suggestions = useMemo(() => { if (similar === undefined) return []; return [...similar.exact, ...similar.similar]; }, [similar]); const submit = async () => { if ( action.pending || title.trim().length === 0 || body.trim().length === 0 ) { return; } if (!allowAuthenticatedAction(isAuthenticated, onUnauthenticated)) return; await action.run(async () => { const metadata = await collectEntryMetadata( collectMetadata, kind, collectStandardMetadata, ); onCreated( await create({ kind, title, body, ...(metadata === undefined ? {} : { metadata }), }), ); }, "Could not submit feedback"); }; const requestSubmit = () => { if ( action.pending || similar === undefined || title.trim().length === 0 || body.trim().length === 0 ) { return; } if (suggestions.length === 0) { void submit(); return; } Alert.alert( similar.exact.length > 0 ? messages.form.exactDuplicate : messages.form.possibleDuplicates, messages.form.duplicateWarning, [ { text: messages.form.cancel, style: "cancel", }, { text: messages.form.submitAnyway, onPress: () => { void submit(); }, }, ], ); }; return ( {enabledKinds.length > 1 && ( <> {messages.form.kind} ({ value, label: messages.kinds[value], }))} value={kind} onValueChange={onKindChange} /> )} {suggestions.length > 0 ? ( {similar?.exact.length ? messages.form.exactDuplicate : messages.form.possibleDuplicates} {suggestions.map((entry) => ( onOpenSuggestion(entry.id)} style={{ gap: 4, padding: 10, borderWidth: 1, borderColor: theme.colors.border, borderRadius: Math.max(8, theme.radius - 2), backgroundColor: theme.colors.surface, }} > {messages.kinds[entry.kind]} {messages.statuses[entry.status]} {entry.title} {entry.body} ▲ {entry.upvoteCount} ·{" "} {messages.entry.comments(entry.commentCount)} ))} ) : null} ); }