@cometchat/chat-uikit-react-native
Version:
Ready-to-use Chat UI Components for React Native
285 lines (284 loc) • 13.7 kB
JavaScript
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