UNPKG

@cometchat/chat-uikit-react-native

Version:

Ready-to-use Chat UI Components for React Native

285 lines (284 loc) 13.7 kB
import { CometChat } from "@cometchat/chat-sdk-react-native"; import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; import { Text, View } from "react-native"; import { CometChatList, localize } from "../shared"; import { CometChatUIEventHandler } from "../shared/events/CometChatUIEventHandler/CometChatUIEventHandler"; import { deepMerge } from "../shared/helper/helperFunctions"; import { Icon } from "../shared/icons/Icon"; import { CometChatTooltipMenu } from "../shared/views/CometChatTooltipMenu"; import { ErrorEmptyView } from "../shared/views/ErrorEmptyView/ErrorEmptyView"; import { useTheme } from "../theme"; import { useThemeInternal } from "../theme/hook"; import { Skeleton } from "./Skeleton"; import { Style } from "./style"; // Unique listener IDs for group events and UI events. const groupListenerId = "grouplist_" + new Date().getTime(); const uiEventListener = "uiEvents_" + new Date().getTime(); /** * CometChatGroups renders a list of groups with search, selection mode, * error/empty/loading views, and a long-press tooltip menu (if you provide menu items). */ export const CometChatGroups = React.forwardRef((props, ref) => { const { AppBarOptions, style = {}, searchPlaceholderText = localize("SEARCH"), showBackButton = false, selectionMode = "none", onSelection = () => { }, onSubmit, hideSearch = false, EmptyView, ErrorView, LoadingView, groupsRequestBuilder, searchRequestBuilder, onError, onBack, onItemPress, onItemLongPress, SubtitleView, ItemView, hideError = false, searchKeyword = "", hideLoadingState, groupTypeVisibility = true, addOptions, options, onEmpty, onLoad, hideHeader, ...newProps } = props; // Theme references. const theme = useTheme(); const { mode } = useThemeInternal(); // Internal ref to CometChatList methods. const groupListRef = useRef(null); // States const [hideSearchError, setHideSearchError] = useState(false); const [selectedGroups, setSelectedGroups] = useState([]); // Tooltip state const [tooltipVisible, setTooltipVisible] = useState(false); const [selectedGroup, setSelectedGroup] = useState(null); const tooltipPosition = useRef({ pageX: 0, pageY: 0 }); // Merge theme styles with any overrides. const mergedStyle = deepMerge(theme.groupStyles, style); /** * Expose imperative methods via ref. */ useImperativeHandle(ref, () => ({ addGroup, updateGroup, removeGroup, getSelectedItems, })); /** * Returns the currently selected group items. */ const getSelectedItems = () => { return selectedGroups; }; /** * Renders an empty state view when no groups are available. * Also triggers `onEmpty` if provided. */ const EmptyStateView = useCallback(() => { useEffect(() => { onEmpty?.(); }, []); return (<View style={{ flex: 1 }}> <ErrorEmptyView title={localize("NO_GROUPS_AVAILABLE")} subTitle={localize("ADD_CONTACTS")} Icon={<Icon name='user-empty-icon' icon={mergedStyle.emptyStateStyle.icon} color={theme.color.neutral300} size={theme.spacing.spacing.s15 << 1} containerStyle={{ marginBottom: theme.spacing.spacing.s5, }}/>} containerStyle={{ flex: 1, justifyContent: "center", alignItems: "center", paddingHorizontal: "10%", }} titleStyle={mergedStyle.emptyStateStyle.titleStyle} subTitleStyle={mergedStyle.emptyStateStyle.subTitleStyle}/> </View>); }, [mergedStyle, theme]); /** * Renders the error state view. * Also hides the search box while this is active. */ const ErrorStateView = useCallback(() => { useEffect(() => { setHideSearchError(true); // Hide search while showing error }, []); return (<View style={{ flex: 1 }}> <ErrorEmptyView title={localize("OOPS")} subTitle={localize("SOMETHING_WENT_WRONG")} tertiaryTitle={localize("WRONG_TEXT_TRY_AGAIN")} Icon={<Icon name='error-state' size={theme.spacing.margin.m15 << 1} containerStyle={{ marginBottom: theme.spacing.margin.m5, }} icon={mergedStyle.errorStateStyle.icon}/>} containerStyle={{ flex: 1, justifyContent: "center", alignItems: "center", paddingHorizontal: "10%", }} titleStyle={mergedStyle.errorStateStyle.titleStyle} subTitleStyle={mergedStyle.errorStateStyle.subTitleStyle}/> </View>); }, [mergedStyle, mode, theme]); /** * Build final list of menu items for a given group: * - If `options` is provided, it overrides everything. * - Otherwise, if `addOptions` is provided, it returns those items only as no default as of now */ const buildMenuItems = (group) => { if (options) { return options(group); } if (addOptions) { return addOptions(group); } // No default menu items, so return empty if no user-defined items. return []; }; /** * Invoked when a group item is long pressed. * If the developer passed `onItemLongPress`, call that and stop. * Otherwise, show the tooltip if there are any menu items for that group. */ const handleItemLongPress = (group, e) => { // Call developer callback if provided if (onItemLongPress) { onItemLongPress(group); return; } // If user has no options / addOptions, no tooltip to show const items = buildMenuItems(group); if (items.length === 0) return; // Save position for the tooltip if (e && e.nativeEvent) { tooltipPosition.current = { pageX: e.nativeEvent.pageX, pageY: e.nativeEvent.pageY, }; } else { // fallback if event coords are missing tooltipPosition.current = { pageX: 200, pageY: 100 }; } // Show tooltip setSelectedGroup(group); setTooltipVisible(true); }; /** * Methods below let you update/manipulate groups in the list. */ const addGroup = (group) => { groupListRef.current.addItemToList((grp) => grp.getGuid() === group.getGuid(), 0); }; const updateGroup = (group) => { groupListRef.current.updateList((grp) => grp.getGuid() === group.getGuid()); }; const removeGroup = (group) => { groupListRef.current?.removeItemFromList(group.getGuid()); }; /** * Group listener callbacks below, to keep the UI synced with group changes. */ const handleGroupMemberRemoval = (...options) => { const group = options[3]; groupListRef.current.updateList(group); }; const handleGroupMemberBan = (...options) => { const group = options[3]; groupListRef.current.updateList(group); }; const handleGroupMemberAddition = (...options) => { const group = options[3]; groupListRef.current.updateList(group); }; const handleGroupMemberScopeChange = (...options) => { const group = options[4]; groupListRef.current.updateList(group); }; /** * Set up group listeners when the component mounts. */ useEffect(() => { CometChat.addGroupListener(groupListenerId, new CometChat.GroupListener({ onGroupMemberScopeChanged: (message, changedUser, newScope, oldScope, changedGroup) => { handleGroupMemberScopeChange(message, changedUser, newScope, oldScope, changedGroup); }, onGroupMemberKicked: (message, kickedUser, kickedBy, kickedFrom) => { handleGroupMemberRemoval(message, kickedUser, kickedBy, kickedFrom); }, onGroupMemberLeft: (message, leavingUser, group) => { handleGroupMemberRemoval(message, leavingUser, null, group); }, onGroupMemberBanned: (message, bannedUser, bannedBy, bannedFrom) => { handleGroupMemberBan(message, bannedUser, bannedBy, bannedFrom); }, onMemberAddedToGroup: (message, userAdded, userAddedBy, userAddedIn) => { handleGroupMemberAddition(message, userAdded, userAddedBy, userAddedIn); }, onGroupMemberJoined: (message, joinedUser, joinedGroup) => { handleGroupMemberAddition(message, joinedUser, null, joinedGroup); }, })); CometChatUIEventHandler.addGroupListener(uiEventListener, { ccGroupCreated: ({ group }) => { groupListRef.current?.addItemToList(group, 0); }, ccGroupDeleted: ({ group }) => { groupListRef.current?.removeItemFromList(group.getGuid()); }, ccGroupLeft: ({ leftGroup }) => { leftGroup.setHasJoined(false); leftGroup.setMembersCount(leftGroup.getMembersCount() - 1); if (leftGroup.getType() === CometChat.GROUP_TYPE.PRIVATE) { groupListRef.current?.removeItemFromList(leftGroup.getGuid()); } else { groupListRef.current?.updateList(leftGroup); } }, ccGroupMemberKicked: ({ kickedFrom }) => { if (kickedFrom?.getType() === CometChat.GROUP_TYPE.PRIVATE) { groupListRef.current?.removeItemFromList(kickedFrom.getGuid()); } else { kickedFrom?.setHasJoined(false); groupListRef.current?.updateList(kickedFrom); } }, ccOwnershipChanged: ({ group }) => { groupListRef.current?.updateList(group); }, ccGroupMemberAdded: ({ userAddedIn }) => { groupListRef.current?.updateList(userAddedIn); }, ccGroupMemberJoined: ({ joinedGroup }) => { joinedGroup.setScope("participant"); joinedGroup.setHasJoined(true); groupListRef.current?.updateList(joinedGroup); }, }); return () => { CometChat.removeGroupListener(groupListenerId); CometChatUIEventHandler.removeGroupListener(uiEventListener); }; }, []); return (<View style={[Style.container, theme.groupStyles.containerStyle]}> <CometChatList hideHeader={hideHeader ?? hideHeader} onItemPress={onItemPress} onItemLongPress={handleItemLongPress} SubtitleView={SubtitleView ? SubtitleView : (group) => (<Text style={[ style.itemStyle?.subtitleStyle, theme.groupStyles.itemStyle?.subtitleStyle, ]}> {group.getMembersCount() + " " + localize(group.getMembersCount() === 1 ? "MEMBER" : "MEMBERS")} </Text>)} statusIndicatorType={(group) => !groupTypeVisibility ? null : group.getType()} title={localize("GROUPS")} hideSearch={hideSearch ? hideSearch : hideSearchError} listStyle={mergedStyle} LoadingView={hideLoadingState ? () => <></> // will not render anything if true : LoadingView ? LoadingView : () => <Skeleton style={mergedStyle.skeletonStyle}/>} EmptyView={EmptyView ? EmptyView : () => <EmptyStateView />} ErrorView={ErrorView ? ErrorView : () => <ErrorStateView />} searchPlaceholderText={searchPlaceholderText} ref={groupListRef} listItemKey='guid' requestBuilder={(groupsRequestBuilder && groupsRequestBuilder.setSearchKeyword(searchKeyword)) || new CometChat.GroupsRequestBuilder().setLimit(30).setSearchKeyword(searchKeyword)} searchRequestBuilder={searchRequestBuilder} AppBarOptions={AppBarOptions} hideBackButton={!showBackButton} selectionMode={selectionMode} onSelection={onSelection} onSubmit={onSubmit} ItemView={ItemView} onError={onError} hideError={hideError} onListFetched={(fetchedList) => { if (fetchedList.length === 0) { onEmpty?.(); } else { onLoad?.(fetchedList); } }} onBack={onBack} {...newProps}/> {/* Tooltip Menu: only shows if selectionMode is "none" and items exist */} {selectedGroup && selectionMode === "none" && tooltipVisible && (<View style={{ position: "absolute", top: tooltipPosition.current.pageY, left: tooltipPosition.current.pageX, zIndex: 9999, }}> <CometChatTooltipMenu visible={tooltipVisible} onClose={() => setTooltipVisible(false)} onDismiss={() => setTooltipVisible(false)} event={{ nativeEvent: tooltipPosition.current, }} menuItems={buildMenuItems(selectedGroup).map((menuItem) => ({ text: menuItem.text, onPress: () => { // Perform the user-defined action, // then close the tooltip. menuItem.onPress(); setTooltipVisible(false); }, icon: menuItem?.icon, textStyle: menuItem?.textStyle, iconStyle: menuItem?.iconStyle, iconContainerStyle: menuItem?.iconContainerStyle, disabled: menuItem.disabled, }))}/> </View>)} </View>); }); //# sourceMappingURL=CometChatGroups.js.map