import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import { ActivityIndicator, FlatList, InteractionManager, Keyboard, KeyboardAvoidingView, NativeModules, Platform, Text, TextInput, TouchableOpacity, TouchableWithoutFeedback, View, } from "react-native"; import { CometChat } from "@cometchat/chat-sdk-react-native"; import { commonVars } from "../../shared/base/vars"; import { Icon } from "../../shared/icons/Icon"; import { useTheme } from "../../theme"; import { useCometChatTranslation } from "../../shared/resources/CometChatLocalizeNew"; import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context"; import { CometChatMessageEvents } from "../../shared/events/CometChatMessageEvents"; import { messageStatus } from "../../shared/utils/CometChatMessageHelper"; const { CommonUtil } = NativeModules; /** * Props for the CometChatCreatePoll component. */ export interface CometChatCreatePollInterface { /** * * * @type {string} * @description Title of the component */ title?: string; /** * * * @type {string} * @description Placeholder text for the poll question input */ questionPlaceholderText?: string; /** * * * @type {(error: CometChat.CometChatException) => void} * @description Callback invoked when an error occurs */ onError?: (error: CometChat.CometChatException) => void; /** * * * @type {CometChat.User} * @description The user for which the poll is being created */ user?: CometChat.User; /** * * * @type {CometChat.Group} * @description The group for which the poll is being created */ group?: CometChat.Group; /** * * * @type {() => void} * @description Callback triggered when the close icon is pressed */ onClose?: () => void; /** * * * @type {string} * @description Placeholder text for the answer inputs */ answerPlaceholderText?: string; /** * * * @type {string} * @description Error message when answer fields are empty */ answerHelpText?: string; /** * * * @type {string} * @description Text for the "Add Answers" button */ addAnswerText?: string; /** * * * @type {number} * @description Default number of answer options */ defaultAnswers?: number; /** * * * @type {CometChat.BaseMessage} * @description The message to reply to */ replyToMessage?: CometChat.BaseMessage; /** * * * @type {() => void} * @description Function to close the reply preview */ closeReplyPreview?: () => void; } /** * CometChatCreatePoll component allows the user to create a poll with a question and multiple answers. * * It validates the poll inputs, handles keyboard behavior, and makes an API call to create the poll. * * @param {CometChatCreatePollInterface} props - The props for the component. * * @returns {JSX.Element} The rendered poll creation UI. */ export const CometChatCreatePoll = (props: CometChatCreatePollInterface) => { const { title, questionPlaceholderText = "Ask question", onError, user, group, onClose, answerPlaceholderText = "Answers", answerHelpText, addAnswerText, defaultAnswers = 2, replyToMessage, closeReplyPreview, } = props; const [question, setQuestion] = useState(""); const [error, setError] = useState(""); const [answers, setAnswers] = useState([]); const [kbOffset, setKbOffset] = useState(59); const [loader, setLoader] = useState(false); const loggedInUser = useRef(null); const theme = useTheme(); const { t } = useCometChatTranslation(); const answerRefs = useRef<(TextInput | null)[]>([]); const [lastRemovedIndex, setLastRemovedIndex] = useState(null); const [newlyAddedIndex, setNewlyAddedIndex] = useState(null); const flatListRef = useRef(null); const insets = useSafeAreaInsets(); const LIMIT_ERROR = t("REACHED_MAX_LIMIT"); /** * Validates the poll question and answer inputs. * * @returns True if validation passes; otherwise, false. */ function validate() { if (!question.trim()) { setError(t("INVALID_POLL_QUESTION")); return false; } const filledAnswers = answers.filter((item) => item.trim() !== ""); const hasEmptyAnswers = answers.some((item) => item.trim() === ""); if (filledAnswers.length < 2) { setError(answerHelpText || t("INVALID_POLL_OPTION")); return false; } if (hasEmptyAnswers) { setError(answerHelpText || t("INVALID_POLL_OPTION")); return false; } setError(""); return true; } /** * Submits the poll by calling the 'polls' extension. */ function polls() { if (!validate()) return; setLoader(true); const payload: any = { question: question, options: answers.filter((item) => item), receiver: user ? user?.getUid() : group ? group?.getGuid() : "", receiverType: user ? "user" : group ? "group" : "", }; if (replyToMessage) { payload.quotedMessageId = replyToMessage.getId(); } if (closeReplyPreview) { closeReplyPreview(); } CometChat.callExtension("polls", "POST", "v2/create", payload) .then((response) => { console.log("poll created", response); onClose && onClose(); setLoader(false); if (replyToMessage) { CometChatMessageEvents.emit(CometChatMessageEvents.ccReplyToMessage, { message: replyToMessage, status: messageStatus.success, }); } }) .catch((error) => { console.log("poll error", error); setLoader(false); setError(t("SOMETHING_WRONG")); onError && onError(error); }); } /** * Renders an error view if any validation error exists. * * @returns The error view or null if no error. */ function ErrorView() { if (!error) return null; return ( {error} ); } /** * Handles changes to the question input. * * @param {string} text - The updated question text. */ function handleQuestionChange(text: string) { setQuestion(text); if (error) { setError(""); } } /** * Handles changes to an answer input. * * @param {string} text - The updated answer text. * @param {number} index - The index of the answer being updated. */ function handleAnswerTextChange(text: string, index: number) { let existingAnswers = [...answers]; existingAnswers[index] = text; setAnswers(existingAnswers); if (error && error !== LIMIT_ERROR) { setError(""); } if (index >= 2 && text.trim() === "") { const previousIndex = index - 1; if (previousIndex >= 0 && answerRefs.current[previousIndex]) { answerRefs.current[previousIndex]?.focus(); } const currentIndex = index; setTimeout(() => { setAnswers((prevAnswers) => { const updatedAnswers = [...prevAnswers]; if ( currentIndex >= 0 && currentIndex < updatedAnswers.length && updatedAnswers[currentIndex].trim() === "" ) { updatedAnswers.splice(currentIndex, 1); } return updatedAnswers; }); }, 100); } } /** * Adds a new answer row if the limit is not reached. */ function handleAddAnswerRow() { if (answers.length < 12) { let existingAnswers = [...answers]; existingAnswers.push(""); setAnswers(existingAnswers); setNewlyAddedIndex(existingAnswers.length - 1); } else { setError(LIMIT_ERROR); } } /** * Renders each answer row. * * @param {{item: string, index: number}} param0 - The answer text and its index. * @returns {JSX.Element} The rendered answer input. */ const renderAnswerItem = ({ item, index }: { item: string; index: number }) => ( true} style={{ flexDirection: "row", width: "100%", alignSelf: "center", justifyContent: "space-between", alignItems: "center", marginTop: 10, }} > { answerRefs.current[index] = el; }} value={item} autoFocus={index === newlyAddedIndex} onChangeText={(text) => handleAnswerTextChange(text, index)} placeholder={answerPlaceholderText} placeholderTextColor={theme.color.textTertiary} style={{ flex: 1, padding: Platform.select({ android: 5, ios: 10 }), borderWidth: 1, borderColor: theme.color.borderLight, borderRadius: 8, paddingLeft: 10, color: theme.color.textPrimary, }} /> ); /** * Renders the "Add Answer" button. * * @returns The add answer button or null if limit reached. */ function AddAnswer() { if (answers.length >= 12) { return ; } return ( {"+ " + (addAnswerText || t("ADD_OPTIONS"))} ); } useLayoutEffect(() => { if (lastRemovedIndex !== null) { const previousIndex = lastRemovedIndex - 1; if (previousIndex >= 0 && answerRefs.current[previousIndex]) { answerRefs.current[previousIndex]?.focus(); } setLastRemovedIndex(null); } }, [lastRemovedIndex]); useEffect(() => { if (newlyAddedIndex !== null) { const timer = setTimeout(() => { if (answerRefs.current[newlyAddedIndex]) { answerRefs.current[newlyAddedIndex]?.focus(); } setNewlyAddedIndex(null); }, 50); return () => clearTimeout(timer); } return; }, [newlyAddedIndex]); useEffect(() => { answerRefs.current = answers.map((_, i) => answerRefs.current[i] || null); }, [answers]); useEffect(() => { let timer: ReturnType; if (answers.length > 0 && flatListRef.current) { timer = setTimeout(() => { flatListRef.current?.scrollToEnd({ animated: true }); }, 50); } return () => { if (timer) clearTimeout(timer); }; }, [answers.length]); useEffect(() => { if (answers.length) { InteractionManager.runAfterInteractions(() => { flatListRef.current?.scrollToEnd({ animated: true }); }); } }, [answers.length]); useEffect(() => { if (error === LIMIT_ERROR && answers.length < 12) { setError(""); } }, [answers, error]); useEffect(() => { const isMaxAndAllFilled = answers.length >= 12 && answers.every((a) => a.trim() !== ""); if (isMaxAndAllFilled) { setError(LIMIT_ERROR); } // do nothing when condition not met — the other effect you already added clears LIMIT_ERROR when answers < 12 }, [answers]); useEffect(() => { let answerslist = new Array(defaultAnswers).fill(""); setAnswers(answerslist); CometChat.getLoggedinUser() .then((u) => (loggedInUser.current = u)) .catch((e) => {}); if (Platform.OS === "ios") { if (Number.isInteger(commonVars.safeAreaInsets.top)) { setKbOffset(commonVars.safeAreaInsets.top!); return; } CommonUtil.getSafeAreaInsets().then((res: any) => { if (Number.isInteger(res.top)) { commonVars.safeAreaInsets.top = res.top; commonVars.safeAreaInsets.bottom = res.bottom; setKbOffset(res.top); } }); } }, []); return ( {/* Header */} {title ? title : t("CREATE_POLL")} {/* Question Input */} {t("QUESTION")} {t("OPTIONS")} {/* Main Content: Answers list */} index.toString()} renderItem={renderAnswerItem} // render a footer only when needed, avoiding a permanently present wrapper that causes extra space ListFooterComponent={() => ( = 12 ? 10 : 20 }}> )} removeClippedSubviews={false} keyboardShouldPersistTaps='always' keyboardDismissMode='interactive' contentContainerStyle={{ paddingBottom: 20, paddingTop: 10, paddingHorizontal: 20, }} automaticallyAdjustContentInsets={false} onScrollToIndexFailed={() => {}} showsVerticalScrollIndicator={false} /> {/* Loader Overlay */} {loader && ( )} {/* Fixed Create Button */} {error && } {t("CREATE")} ); };