@cometchat/chat-uikit-react-native
Version:
Ready-to-use Chat UI Components for React Native
493 lines • 21.5 kB
JavaScript
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