@cometchat/chat-uikit-react-native
Version:
Ready-to-use Chat UI Components for React Native
339 lines • 13.4 kB
JavaScript
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { ActivityIndicator, FlatList, Keyboard, KeyboardAvoidingView, NativeModules, Platform, SafeAreaView, 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 { localize } from "../../shared/resources/CometChatLocalize";
import { useTheme } from "../../theme";
const { CommonUtil } = NativeModules;
/**
* 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) => {
const { title, questionPlaceholderText = "Ask question", onError, user, group, onClose, answerPlaceholderText = "Answers", answerHelpText, addAnswerText, defaultAnswers = 2, } = 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 answerRefs = useRef([]);
const [lastRemovedIndex, setLastRemovedIndex] = useState(null);
const [focusedIndex, setFocusedIndex] = useState(null);
/**
* Validates the poll question and answer inputs.
*
* @returns True if validation passes; otherwise, false.
*/
function validate() {
if (!question.trim()) {
setError(localize("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 || localize("INVALID_POLL_OPTION"));
return false;
}
if (hasEmptyAnswers) {
setError(answerHelpText || localize("INVALID_POLL_OPTION"));
return false;
}
setError("");
return true;
}
/**
* Submits the poll by calling the 'polls' extension.
*/
function polls() {
if (!validate())
return;
setLoader(true);
CometChat.callExtension("polls", "POST", "v2/create", {
question: question,
options: answers.filter((item) => item),
receiver: user ? user?.getUid() : group ? group?.getGuid() : "",
receiverType: user ? "user" : group ? "group" : "",
})
.then((response) => {
console.log("poll created", response);
onClose && onClose();
setLoader(false);
})
.catch((error) => {
console.log("poll error", error);
setLoader(false);
setError(localize("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 (<View style={{
flexDirection: "row",
alignItems: "center",
borderRadius: 8,
backgroundColor: "rgba(255, 59, 48, 0.1)",
justifyContent: "center",
marginBottom: 10,
paddingVertical: 10,
paddingHorizontal: 15,
}}>
<Icon name="info" color={theme.color.error} size={20}/>
<Text style={[
theme.typography.caption1.regular,
{ color: theme.color.error, marginLeft: 10 },
]}>
{error}
</Text>
</View>);
}
/**
* Handles changes to the question input.
*
* @param {string} text - The updated question text.
*/
function handleQuestionChange(text) {
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, index) {
let existingAnswers = [...answers];
existingAnswers[index] = text;
setAnswers(existingAnswers);
if (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);
setFocusedIndex(existingAnswers.length - 1);
}
else {
setError("You can only add up to 12 options.");
}
}
/**
* 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 }) => (<View onStartShouldSetResponder={() => true} style={{
flexDirection: "row",
width: "100%",
alignSelf: "center",
justifyContent: "space-between",
alignItems: "center",
marginTop: 10,
}}>
<TextInput ref={(el) => {
answerRefs.current[index] = el;
}} value={item} 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,
}}/>
</View>);
/**
* Renders the "Add Answer" button.
*
* @returns The add answer button or null if limit reached.
*/
function AddAnswer() {
if (answers.length >= 12) {
return null;
}
return (<TouchableOpacity onPress={handleAddAnswerRow} style={{
height: 56,
width: "100%",
justifyContent: "center",
alignItems: "flex-start",
paddingLeft: 10,
}}>
<Text style={[
theme.typography.caption1.medium,
{ color: theme.color.primary, textAlign: "center" },
]}>
{"+ " + (addAnswerText || localize("ADD_OPTIONS"))}
</Text>
</TouchableOpacity>);
}
useLayoutEffect(() => {
if (lastRemovedIndex !== null) {
const previousIndex = lastRemovedIndex - 1;
if (previousIndex >= 0 && answerRefs.current[previousIndex]) {
answerRefs.current[previousIndex]?.focus();
}
setLastRemovedIndex(null);
}
}, [lastRemovedIndex]);
useEffect(() => {
if (focusedIndex !== null && answerRefs.current[focusedIndex]) {
answerRefs.current[focusedIndex]?.focus();
setFocusedIndex(null);
}
}, [focusedIndex]);
useEffect(() => {
answerRefs.current = answers.map((_, i) => answerRefs.current[i] || null);
}, [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) => {
if (Number.isInteger(res.top)) {
commonVars.safeAreaInsets.top = res.top;
commonVars.safeAreaInsets.bottom = res.bottom;
setKbOffset(res.top);
}
});
}
}, []);
return (<SafeAreaView style={{ flex: 1, backgroundColor: theme.color.background1 }}>
<KeyboardAvoidingView style={{ flex: 1 }} enabled={Platform.OS === "ios"} behavior={Platform.select({ ios: "padding", android: "height" })}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={{ flex: 1 }}>
{/* Header */}
<View style={{ flexDirection: "row", alignItems: "center" }}>
<TouchableOpacity onPress={onClose} style={{ flexDirection: "row", paddingVertical: 20, paddingLeft: 10 }}>
<Icon name="arrow-back"/>
</TouchableOpacity>
<Text style={[
theme.typography.heading2.bold,
{ color: theme.color.iconPrimary, paddingLeft: 10 },
]}>
{title ? title : localize("CREATE_POLL")}
</Text>
</View>
{/* Question Input */}
<View style={{ borderTopWidth: 1, borderColor: theme.color.borderLight }}>
<View style={{ paddingHorizontal: 20 }}>
<Text style={[
theme.typography.heading4.medium,
{ marginTop: 15, color: theme.color.textPrimary },
]}>
{localize("QUESTION")}
</Text>
<TextInput value={question} onChangeText={handleQuestionChange} placeholder={questionPlaceholderText} placeholderTextColor={theme.color.textTertiary} style={[
{
padding: Platform.select({ android: 5, ios: 10 }),
borderWidth: 1,
borderColor: theme.color.borderLight,
borderRadius: 8,
marginTop: 10,
paddingLeft: 10,
color: theme.color.textPrimary,
},
]}/>
<Text style={[
theme.typography.heading4.medium,
{ marginTop: 25, color: theme.color.textPrimary, marginBottom: 2 },
]}>
{localize("OPTIONS")}
</Text>
</View>
</View>
{/* Main Content: Answers list */}
<View style={{ flex: 1 }}>
<FlatList data={answers} keyExtractor={(item, index) => index.toString()} renderItem={renderAnswerItem} ListFooterComponent={<View style={{ paddingBottom: 20 }}><AddAnswer /></View>} removeClippedSubviews={false} keyboardShouldPersistTaps="always" keyboardDismissMode="interactive" contentContainerStyle={{
paddingBottom: 100,
paddingTop: 10,
paddingHorizontal: 20,
}} automaticallyAdjustContentInsets={false} onScrollToIndexFailed={() => { }} showsVerticalScrollIndicator={false}/>
</View>
</View>
</TouchableWithoutFeedback>
{/* Loader Overlay */}
{loader && (<View style={{
position: "absolute",
height: "100%",
width: "100%",
zIndex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0,0,0,0.3)",
}}>
<ActivityIndicator size="large" color={theme.color.primary}/>
</View>)}
{/* Fixed Create Button */}
<View style={{
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: theme.color.background1,
}}>
{error && <ErrorView />}
<TouchableOpacity onPress={polls} style={{
width: "100%",
backgroundColor: theme.color.primaryButtonBackground,
alignItems: "center",
borderRadius: 8,
paddingVertical: 12,
marginBottom: Platform.select({ ios: 0, android: 10 }),
}}>
<Text style={[theme.typography.button.medium, { color: theme.color.primaryButtonText }]}>
{localize("CREATE")}
</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>);
};
//# sourceMappingURL=Polls.js.map