UNPKG

@cometchat/chat-uikit-react-native

Version:

Ready-to-use Chat UI Components for React Native

493 lines 21.5 kB
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; import { ActivityIndicator, FlatList, Text, View, } from "react-native"; import { LOADING, NO_DATA_FOUND, SOMETHING_WRONG } from "../../constants/UIKitConstants"; import { localize } from "../../resources/CometChatLocalize"; import { CometChat } from "@cometchat/chat-sdk-react-native"; import { useTheme } from "../../../theme"; import { CometChatUIKit } from "../../CometChatUiKit"; import { Icon } from "../../icons/Icon"; import { CometChatListItem } from "../CometChatListItem"; import Header from "./Header"; import styles from "./styles"; let lastCall; let lastReject; /** * @class Users is a component useful for displaying the header and users in a list * @description This component displays a header and list of users with subtitle,avatar,status * @Version 1.0.0 * @author CometChat * */ export const CometChatList = React.forwardRef((props, ref) => { const connectionListenerId = "connectionListener_" + new Date().getTime(); const theme = useTheme(); const [isLoadingMore, setIsLoadingMore] = useState(false); const [hasMoreData, setHasMoreData] = useState(true); const { LeadingView, TitleView, SubtitleView, TrailingView, disableUsersPresence = false, ItemView, AppBarOptions, searchPlaceholderText, hideBackButton, selectionMode = "none", onSelection = () => { }, onSubmit, hideSearch = false, title = "Title", EmptyView, emptyStateText = localize("NO_USERS_FOUND"), errorStateText = localize("SOMETHING_WRONG"), ErrorView, LoadingView, requestBuilder, hideHeader, searchRequestBuilder = undefined, hideError = false, onItemPress = () => { }, onItemLongPress = () => { }, onError, onBack, listItemKey = "uid", listStyle, hideSubmitButton, statusIndicatorType, hideStickyHeader = false, } = props; // functions which can be accessed by parents useImperativeHandle(ref, () => { return { updateList, addItemToList, removeItemFromList, getListItem, updateAndMoveToFirst, getSelectedItems, getAllListItems, }; }); const [searchInput, setSearchInput] = useState(requestBuilder ? requestBuilder.searchKeyword ? requestBuilder.searchKeyword : "" : searchRequestBuilder ? searchRequestBuilder.searchKeyword ? searchRequestBuilder.searchKeyword : "" : ""); const searchInputRef = useRef(requestBuilder ? requestBuilder.searchKeyword ? requestBuilder.searchKeyword : "" : searchRequestBuilder ? searchRequestBuilder.searchKeyword ? searchRequestBuilder.searchKeyword : "" : ""); const [selectedItems, setSelectedItems] = useState({}); const [shouldSelect, setShouldSelect] = useState(props.selectionMode === "single" || props.selectionMode === "multiple"); const listHandlerRef = useRef(null); const initialRunRef = useRef(true); const [list, setList] = useState([]); const [dataLoadingStatus, setDataLoadingStatus] = useState(LOADING); useEffect(() => { if (props.onSelection) { const selectedArray = Object.values(selectedItems); props.onSelection(selectedArray); } }, [selectedItems]); // Debounced search handler const searchHandler = (searchText) => { setSearchInput(searchText); setHasMoreData(true); let _searchRequestBuilder = searchRequestBuilder || requestBuilder; if (searchRequestBuilder && searchText) { _searchRequestBuilder = searchRequestBuilder.setSearchKeyword(searchText).build(); } else if (requestBuilder) { _searchRequestBuilder = requestBuilder.setSearchKeyword(searchText).build(); } getSearch(_searchRequestBuilder); }; const getSearch = (builder) => { getList(builder) .then((newlist) => { setDataLoadingStatus(NO_DATA_FOUND); setList(newlist); }) .catch((error) => { if (error && error["message"] == "Promise cancelled") { // Handle promise cancellation if necessary } else { setDataLoadingStatus(SOMETHING_WRONG); errorHandler(error); } }); }; const getSelectedItems = () => { let markedItems = []; Object.keys(selectedItems).forEach((item) => { const listItem = getListItem(item); if (listItem) markedItems.push(listItem); }); return markedItems; }; useEffect(() => { CometChat.addConnectionListener(connectionListenerId, new CometChat.ConnectionListener({ onConnected: () => { if (requestBuilder) { if (searchInputRef.current) listHandlerRef.current = requestBuilder .setSearchKeyword(searchInputRef.current) .build(); else listHandlerRef.current = requestBuilder.build(); } getList(listHandlerRef.current) .then((newlist) => { setDataLoadingStatus(NO_DATA_FOUND); setList(newlist); }) .catch((error) => { if (error && error["message"] === "Promise cancelled") { // Handle promise cancellation if necessary } else { setDataLoadingStatus(SOMETHING_WRONG); errorHandler(error); } }); }, inConnecting: () => { console.log("ConnectionListener => In connecting"); }, onDisconnected: () => { console.log("ConnectionListener => On Disconnected"); }, })); return () => { CometChat.removeConnectionListener(connectionListenerId); }; }, []); useEffect(() => { if (initialRunRef.current === true) { if (requestBuilder) { if (searchInput) listHandlerRef.current = requestBuilder.setSearchKeyword(searchInput).build(); else listHandlerRef.current = requestBuilder.build(); } initialRunRef.current = false; handleList(false); } }, []); useEffect(() => { searchInputRef.current = searchInput; }, [searchInput]); /** * Updates the list of users to be displayed * @param */ const updateList = (item) => { let newList = [...list]; let itemKey = newList.findIndex((u) => u[listItemKey] === item[listItemKey]); if (itemKey > -1) { newList.splice(itemKey, 1, item); if (newList.length === 0) setDataLoadingStatus(NO_DATA_FOUND); setList(newList); } }; /** * This will move item to first location if item doesn't exist then add it to first location. * @param item */ const updateAndMoveToFirst = (item) => { let newList = [...list]; let itemKey = newList.findIndex((u) => u[listItemKey] === item[listItemKey]); if (itemKey > -1) { newList.splice(itemKey, 1); } setList([item, ...newList]); }; const addItemToList = (item, position) => { setList((prev) => { if (position !== undefined) { if (position === 0) return [item, ...prev]; if (position >= prev.length) return [...prev, item]; else return [...prev.slice(0, position - 1), item, ...prev.slice(position)]; } return [...prev, item]; }); }; const removeItemFromList = (uid) => { setList((prev) => { let newList = prev.filter((item) => item[listItemKey] !== uid); if (newList.length === 0) setDataLoadingStatus(NO_DATA_FOUND); return newList; }); if (ItemView === undefined && shouldSelect) { let newSelectedItems = { ...selectedItems }; if (Object.keys(selectedItems).includes(uid.toString())) { delete newSelectedItems[uid]; setSelectedItems(newSelectedItems); } } }; const getListItem = (itemId) => { return list.find((item) => item[listItemKey] === itemId); }; /** * Get all list items */ const getAllListItems = () => { return list; }; /** * Handle list fetching with pagination * @param {boolean} throughKeyword - Pass true if wants to set only new users. */ const handleList = (throughKeyword) => { // Prevent multiple fetches if (isLoadingMore || (!throughKeyword && !hasMoreData)) return; setIsLoadingMore(true); getList(listHandlerRef.current) .then((newlist = []) => { let finalList = []; if (throughKeyword || list.length === 0) { // If we're resetting the list or there is no existing list if (throughKeyword) setHasMoreData(true); else if (newlist.length === 0) setHasMoreData(false); finalList = newlist; if (finalList.length === 0) { setDataLoadingStatus(NO_DATA_FOUND); } else { setDataLoadingStatus(""); } setList(finalList); } else { // Append to existing list finalList = [...list, ...newlist]; setList(finalList); // When the backend returns nothing more, mark the end of data if (newlist.length === 0) setHasMoreData(false); } // If we *did* get results but less than a full “page”, also stop further loads if (newlist.length === 0) setHasMoreData(false); props.onListFetched?.(finalList); setIsLoadingMore(false); }) .catch((error) => { if (error && error["message"] === "Promise cancelled") { // Handle promise cancellation if necessary } else { setDataLoadingStatus(SOMETHING_WRONG); errorHandler(error); } setIsLoadingMore(false); }); }; const renderFooter = useCallback(() => { if (!isLoadingMore || !hasMoreData) return null; return (<View style={styles.footerContainer}> <ActivityIndicator size='small' color={theme.color.primary}/> </View>); }, [isLoadingMore, hasMoreData]); /** * Handle errors */ const errorHandler = (errorCode) => { onError && onError(errorCode); // CometChatUserEvents.emit(CometChatUserEvents.onUserError, errorCode); }; /** * Handle item selection based on selection mode */ const handleSelection = useCallback((listItem) => { if (selectionMode === "none") return; const itemKey = listItem.value[listItemKey]; setSelectedItems((prev) => { let newState = { ...prev }; if (selectionMode === "multiple") { if (newState[itemKey]) { delete newState[itemKey]; } else { newState[itemKey] = listItem.value; } } else if (selectionMode === "single") { if (newState[itemKey]) { delete newState[itemKey]; } else { newState = { [itemKey]: listItem.value }; } } // Notify parent about selection change return newState; }); }, [selectionMode, onSelection, listItemKey]); /** * Handle Cancel action */ const handleCancelSelection = () => { setSelectedItems({}); }; /** * Handle Confirm action */ const handleConfirmSelection = () => { onSubmit && onSubmit(Object.values(selectedItems)); // Optionally, you might want to clear the selection after confirmation setSelectedItems({}); }; const onListItemPress = (item) => { if (shouldSelect) { handleSelection(item); } else { onItemPress(item.value); } }; const onListItemLongPress = (item, e) => { handleSelection(item); onItemLongPress(item.value, e); }; const selectedCount = Object.keys(selectedItems).length; const renderItemView = useCallback(({ item, index }) => { if (item.header) { if (hideStickyHeader) return null; const headerLetter = item.value; return (<View key={`header_${headerLetter}_${index}`}> <Text style={[styles.headerLetterStyle, listStyle?.sectionHeaderTextStyle]}> {headerLetter} </Text> </View>); } return (<CometChatListItem statusIndicatorType={statusIndicatorType ? statusIndicatorType(item.value) : null} id={item.value[listItemKey]} avatarName={item.value.name} selected={!!selectedItems[item.value[listItemKey]]} shouldSelect={shouldSelect} LeadingView={LeadingView && LeadingView(item.value)} TitleView={TitleView && TitleView(item.value)} title={item.value.uid && item.value.uid === CometChatUIKit.loggedInUser.getUid() && item.value.name === CometChatUIKit.loggedInUser.getName() ? localize("YOU") : item.value.name} containerStyle={[listStyle?.itemStyle?.containerStyle]} titleStyle={listStyle?.itemStyle?.titleStyle} headViewContainerStyle={listStyle?.itemStyle?.headViewContainerStyle ?? { marginHorizontal: 9 }} titleSubtitleContainerStyle={listStyle?.itemStyle?.titleSubtitleContainerStyle ?? {}} trailingViewContainerStyle={listStyle?.itemStyle?.trailingViewContainerStyle ?? {}} avatarURL={item.value.avatar || undefined} avatarStyle={listStyle?.itemStyle?.avatarStyle} SubtitleView={SubtitleView ? SubtitleView(item.value) : undefined} TrailingView={TrailingView ? TrailingView(item.value) : undefined} onPress={() => { onListItemPress(item); }} // onLongPress={() => { // onListItemLongPress(item); // }} onLongPress={(id, e) => { // const listItem = getListItem(id); onListItemLongPress(item, e); }}/>); }, [ listItemKey, selectedItems, shouldSelect, listStyle, SubtitleView, TitleView, TrailingView, disableUsersPresence, theme, onListItemPress, onListItemLongPress, hideStickyHeader, LeadingView, ]); /** * Gets the list of users */ const getList = (reqBuilder) => { const promise = new Promise((resolve, reject) => { const cancel = () => { clearTimeout(lastCall); lastReject(new Error("Promise cancelled")); }; if (lastCall) { cancel(); } lastCall = setTimeout(() => { reqBuilder ?.fetchNext() .then((listItems) => { resolve(listItems); }) .catch((error) => { reject(error); }); }, 500); lastReject = reject; }); return promise; }; /** * Returns a container of users if exists else returns the corresponding decorator message */ const getMessageContainer = useCallback(() => { let messageContainer = <></>; if (list.length === 0 && dataLoadingStatus.toLowerCase() === NO_DATA_FOUND) { messageContainer = EmptyView ? (<EmptyView />) : (<View style={[styles.msgContainerStyle, listStyle?.emptyStateStyle?.containerStyle]}> <Text style={[styles.msgTxtStyle, listStyle?.emptyStateStyle?.titleStyle]}> {emptyStateText} </Text> </View>); } else if (!hideError && dataLoadingStatus.toLowerCase() === SOMETHING_WRONG) { messageContainer = ErrorView ? (<ErrorView />) : (<View style={[styles.msgContainerStyle, listStyle?.errorStateStyle?.containerStyle]}> {listStyle?.errorStateStyle?.icon && (<Icon name='error' size={24} color={theme.color.error}/>)} <Text style={[styles.msgTxtStyle, listStyle?.errorStateStyle?.titleStyle]}> {errorStateText} </Text> </View>); } else { let currentLetter = ""; const listWithHeaders = []; if (list.length) { list.forEach((listItem) => { const chr = listItem?.name && listItem.name[0].toUpperCase(); if (!hideStickyHeader && chr !== currentLetter && !ItemView) { currentLetter = chr; listWithHeaders.push({ value: currentLetter, header: true, }); } listWithHeaders.push({ value: listItem, header: false }); }); messageContainer = (<View style={styles.listContainerStyle}> <FlatList data={listWithHeaders} extraData={{ selectedItems, theme, list, selectionMode }} renderItem={ItemView ? ({ item, index, separators }) => { return ItemView(item?.value); } : renderItemView} keyExtractor={(item, index) => { const itemValue = { ...item.value }; let key = itemValue[listItemKey] ? `${itemValue[listItemKey]}` : undefined; if (!key) { //section header is also an item in the list if (itemValue[0] && itemValue[0].length === 1) { key = itemValue[0] + "_" + index; } } return key ?? index + ""; }} onMomentumScrollEnd={(event) => { const contentOffsetY = event.nativeEvent.contentOffset.y; const contentHeight = event.nativeEvent.contentSize.height; const layoutHeight = event.nativeEvent.layoutMeasurement.height; if (contentOffsetY + layoutHeight >= contentHeight - 10) { handleList(); } }} showsVerticalScrollIndicator={false} ListFooterComponent={renderFooter} keyboardShouldPersistTaps='always'/> </View>); } } return messageContainer; }, [list, selectedItems, theme, dataLoadingStatus, isLoadingMore, hasMoreData, shouldSelect]); /** * Handle the rendering based on loading status */ // if (list.length === 0 && dataLoadingStatus.toLowerCase() === LOADING) { // if (LoadingView) return <LoadingView />; // } else { return (<View style={{ flex: 1 }}> <Header hideHeader={hideHeader ?? hideHeader} backButtonIconContainerStyle={listStyle?.backButtonIconContainerStyle} backButtonIcon={listStyle?.backButtonIcon} backButtonIconStyle={listStyle?.backButtonIconStyle} hideBackButton={hideBackButton} containerStyle={listStyle?.headerContainerStyle} titleSeparatorStyle={listStyle?.titleSeparatorStyle} titleViewStyle={listStyle?.titleViewStyle} onBack={onBack} title={title} titleStyle={listStyle?.titleStyle} AppBarOptions={AppBarOptions} shouldSelect={shouldSelect} hideSubmitButton={hideSubmitButton} onCancel={handleCancelSelection} onConfirm={handleConfirmSelection} hideSearch={hideSearch} searchPlaceholderText={searchPlaceholderText} searchHandler={searchHandler} searchInput={searchInput} onSubmitEditing={() => searchHandler(searchInput)} selectionCancelStyle={listStyle?.selectionCancelStyle} confirmSelectionStyle={listStyle?.confirmSelectionStyle} searchStyle={listStyle?.searchStyle} selectedCount={selectedCount}/> <View style={styles.container}>{getMessageContainer()}</View> {list.length === 0 && dataLoadingStatus.toLowerCase() === LOADING ? (LoadingView ? (<LoadingView />) : (<View style={styles.msgContainerStyle}> <ActivityIndicator size={theme.spacing.spacing.s10} color={listStyle?.loadingIconTint || theme.color.iconSecondary}/> </View>)) : null} {/* Add a fallback like `null` if nothing needs to be rendered */} </View>); } // } ); //# sourceMappingURL=CometChatList.js.map