Closed jasimawan closed 2 months ago
Afroz Khan (CometChat Team) replied:
Hi Jasimawan,
Could you please share how you've implemented the CometChatMessageList
component and Custom MessageComposer
.
Afroz Khan CometChat
MessageComposer
import { useTheme, useThemedStyle } from '@/src/theme';
import { styleProvider } from './style';
import { View } from '../../../View';
import { StyleProp, ViewStyle } from 'react-native';
import { Input } from '../../../Input';
import { IconButton } from '../../../IconButton';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
export interface MessageComposerProps {
userName: string;
style?: StyleProp<ViewStyle>;
onSendMessage: (text: string) => void;
onSendImage: () => void;
onSendVoice: () => void;
loading?: boolean;
}
export const MessageComposer = ({
userName,
onSendImage,
onSendMessage,
onSendVoice,
style,
loading,
}: MessageComposerProps) => {
const { t } = useTranslation();
const styles = useThemedStyle(styleProvider);
const theme = useTheme();
const [text, setText] = useState('');
const handleSendMessage = () => {
if (text?.trim() === '') return;
onSendMessage(text.trim());
setText('');
};
return (
<View style={[styles.composer, style]}>
<View style={styles.inputContainer}>
<Input
style={styles.inputView}
inputStyle={styles.inputStyle}
placeholder={`${t('replyTo')} ${userName}`}
onChangeText={(text: string) => setText(text)}
disabled={loading}
value={text}
multiline
numberOfLines={4}
autoCapitalize="sentences"
/>
<IconButton
style={styles.sendImage}
icon="icon-image-fill"
iconSize={24}
iconColor={theme.pallet.neutral500}
onPress={onSendImage}
bgColors={['transparent']}
disabled={loading}
/>
<IconButton
style={styles.voiceRecording}
icon="icon-microphone-fill"
iconSize={24}
iconColor={theme.pallet.neutral500}
onPress={onSendVoice}
bgColors={['transparent']}
disabled={loading}
/>
</View>
<IconButton
style={styles.sendMessage}
containerStyle={styles.sendMessage}
icon="icon-paper-plane-tilt-fill"
iconSize={32}
onPress={handleSendMessage}
loading={loading}
disabled={text?.trim() === ''}
/>
</View>
);
};
CometChatMessageList
/* eslint-disable react/display-name */
import { useMessagesController } from './useMessagesController';
import { ss, useTheme, useThemedStyle } from '@/src/theme';
import { styleProvider } from './style';
import {
Header,
Screen,
View,
Text,
Image,
RecordAudioModal,
Divider,
MessageComposer,
CertificationModal,
VoiceMessage,
CompactUserCard,
} from '@/src/components';
import { Loader } from '@/src/components/Loader';
import {
ChatConfigurator,
CometChatMessageList,
CometChatMessageTemplate,
CometChatTheme,
MessageBubbleAlignmentType,
} from '@cometchat/chat-uikit-react-native';
import { useCallback, useState } from 'react';
import { CometChat } from '@cometchat/chat-sdk-react-native';
import { useTranslation } from 'react-i18next';
import moment from 'moment';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Dimensions, KeyboardEvent, Platform, StatusBar } from 'react-native';
import { ScrollView, TouchableOpacity } from 'react-native-gesture-handler';
import { useKeyboardDidHide } from '@/src/services/hooks';
import { NormalButton } from '@/src/components';
import Tooltip from 'react-native-walkthrough-tooltip';
import Hyperlink from 'react-native-hyperlink';
const HEADER_HEIGHT = 56;
const COMPOSER_HEIGHT = 55;
const DIVIDER_HEIGHT = 24;
const ONE_SECOND = 1000;
type BroadCastedMessage = {
broadcastMessage?: boolean;
};
export const MessagesScreen = () => {
const { t } = useTranslation();
const theme = useTheme();
const styles = useThemedStyle(styleProvider);
const { bottom, top } = useSafeAreaInsets();
const cometChatTheme = new CometChatTheme({});
const {
chatUser: user,
group,
isGroup,
goBack,
userName,
isArchived,
handleOpenModal,
handleCloseModal,
isVisible,
handleSendMessage,
handleSubmitFile,
onSelectPhoto,
cometChatMessageListRef,
loading,
sending,
messageImage,
setMessageImage,
setShowTooltip,
showTooltip,
groupUsers,
onOpenMessageLink,
} = useMessagesController();
const [keyboardHeight, setKeyboardHeight] = useState(0);
const archivedHeight = isArchived ? ss(48) : 0;
const groupHeight = isGroup ? ss(80) : 0;
const getChatTemplate = useCallback(() => {
const templates: CometChatMessageTemplate[] =
ChatConfigurator.getDataSource().getAllMessageTemplates(cometChatTheme);
return templates.map((data) => {
data.HeaderView = (
messageObject: CometChat.BaseMessage,
alignment: MessageBubbleAlignmentType,
) => {
const metadata = (messageObject as CometChat.TextMessage).getMetadata();
const isBroadcasted = (metadata as unknown as BroadCastedMessage)
.broadcastMessage;
const text =
alignment === 'left' ? messageObject.getSender().getName() : t('you');
return (
<View style={styles.messageHeader}>
<Text style={styles.name}>
{isBroadcasted ? `${text} (${t('broadcastMessage')})` : text}
</Text>
<Text style={styles.date}>
{moment(messageObject.getSentAt() * ONE_SECOND).format(
'dddd, h:mm a',
)}
</Text>
</View>
);
};
data.ContentView = (
messageObject: CometChat.BaseMessage,
alignment: MessageBubbleAlignmentType,
) => {
const metadata = (messageObject as CometChat.TextMessage).getMetadata();
const isBroadcasted = (metadata as unknown as BroadCastedMessage)
.broadcastMessage;
return (
<View
style={[
alignment === 'left' ? styles.messageLeft : styles.messageRight,
isBroadcasted && styles.broadcastBG,
]}
>
{data.type === CometChat.MESSAGE_TYPE.TEXT && (
<Hyperlink onPress={onOpenMessageLink} linkStyle={styles.link}>
<Text
style={
alignment === 'left' || isBroadcasted
? styles.messageTextLeft
: styles.messageTextRight
}
>
{`${(messageObject as CometChat.TextMessage).getText()}`}
</Text>
</Hyperlink>
)}
{data.type === 'image' && (
<TouchableOpacity
onPress={() => setMessageImage(messageObject?.getData()?.url)}
>
<Image
style={styles.bubbleImage}
resizeMode="cover"
source={{
uri: messageObject?.getData()?.url,
}}
/>
{(messageObject as CometChat.MediaMessage).getCaption() && (
<Hyperlink
onPress={onOpenMessageLink}
linkStyle={styles.link}
>
<Text
style={[
styles.caption,
alignment === 'left' || isBroadcasted
? styles.messageTextLeft
: styles.messageTextRight,
]}
>
{`${(messageObject as CometChat.MediaMessage).getCaption()}`}
</Text>
</Hyperlink>
)}
</TouchableOpacity>
)}
{data.type === 'audio' && (
<VoiceMessage
id={messageObject?.getId()}
url={messageObject?.getData()?.url}
isLeft={alignment === 'left'}
/>
)}
</View>
);
};
data.StatusInfoView = () => <></>;
data.options = () => [];
return data;
});
}, [cometChatTheme, user, group, setMessageImage]);
const handleKeyboardStateChange = (
state: 'show' | 'hide',
event?: KeyboardEvent,
) => {
if (!event || !event.endCoordinates) {
setKeyboardHeight(0);
return;
}
const isShow = state === 'show';
isShow && cometChatMessageListRef.current?.scrollToBottom();
setKeyboardHeight(isShow ? event.endCoordinates.height : 0);
};
useKeyboardDidHide(handleKeyboardStateChange);
const renderLoader = () => {
return (
<View style={styles.loader}>
<Loader />
</View>
);
};
return (
<Screen>
<Header
title={userName}
onPressBackArrow={goBack}
showBackArrow
hideMessageIcon
/>
<View style={styles.messagesContainer}>
{isArchived && (
<View style={styles.archivedView}>
<Text style={styles.archived}>{`- ${t('archived')} -`}</Text>
</View>
)}
{isGroup && (
<Tooltip
isVisible={showTooltip}
content={
<ScrollView>
{groupUsers.map((groupUser) => (
<CompactUserCard
key={groupUser.id}
name={`${groupUser.firstName} ${groupUser.lastName}`}
jobType={groupUser.workSchedule || ''}
status={
groupUser.onboardingStage ||
'awaiting-company-application'
}
/>
))}
</ScrollView>
}
placement="bottom"
onClose={() => setShowTooltip(false)}
contentStyle={styles.tooltipContent}
childContentSpacing={0}
arrowSize={{ height: 0, width: 0 }}
backgroundColor="transparent"
closeOnChildInteraction={false}
closeOnContentInteraction={false}
closeOnBackgroundInteraction={false}
parentWrapperStyle={styles.tooltipParent}
tooltipStyle={styles.tooltip}
topAdjustment={
Platform.OS === 'android' ? -StatusBar.currentHeight! : 0
}
>
<NormalButton
onPress={() => setShowTooltip((prev) => !prev)}
style={styles.messageButton}
icon={showTooltip ? 'icon-caret-up-bold' : 'icon-caret-down-bold'}
iconColor={theme.pallet.primary500}
iconSize={20}
textStyle={styles.text}
title={t('broadcastedUsers')}
position="right"
/>
</Tooltip>
)}
{loading ? (
renderLoader()
) : (
<>
<View
style={[
styles.messageList,
{
height:
Dimensions.get('window').height -
top -
bottom -
ss(HEADER_HEIGHT) -
ss(DIVIDER_HEIGHT) -
ss(COMPOSER_HEIGHT) -
archivedHeight -
groupHeight -
keyboardHeight,
},
]}
>
<CometChatMessageList
ref={cometChatMessageListRef}
{...(isGroup ? { group } : { user })}
messageListStyle={styles.messageList}
wrapperMessageBubbleStyle={styles.messageBubbleWrapper}
LoadingStateView={renderLoader}
scrollToBottomOnNewMessages
templates={getChatTemplate()}
disableSoundForMessages
avatarStyle={{
backgroundColor: theme.pallet.neutral500,
nameTextColor: theme.pallet.white,
}}
dateSeperatorStyle={{
textColor: theme.pallet.neutral500,
textFont: { fontFamily: theme.font.semiBold },
}}
/>
</View>
<Divider style={styles.divider} />
<MessageComposer
userName={userName || ''}
onSendImage={onSelectPhoto}
onSendMessage={handleSendMessage}
onSendVoice={handleOpenModal}
loading={sending}
/>
</>
)}
</View>
{isVisible && (
<RecordAudioModal
isVisible={isVisible}
onClose={handleCloseModal}
onSubmit={handleSubmitFile}
/>
)}
{!!messageImage && (
<CertificationModal
isVisible={!!messageImage}
onClose={() => setMessageImage(undefined)}
url={messageImage}
/>
)}
</Screen>
);
};
This is our controller logic which handles everything in CometChatMessageList screen
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useCallback, useEffect, useRef, useState } from 'react';
import { CometChat } from '@cometchat/chat-sdk-react-native';
import { Linking, Platform } from 'react-native';
import { CometChatMessageListActionsInterface } from '@cometchat/chat-uikit-react-native';
import { useAppSelector } from '@/src/store/hooks';
import {
showActionSheetAndroid,
showActionSheetIOS,
} from '@/src/utils/imagePicker';
import { useGetBroadcastedUsers } from '@/src/services/hooks';
import { mapWorkforceUsersForMessages } from '@/src/utils/chat';
import { WorkScheduleType } from '@shared/types/workScheduleType.type';
type GroupUsersList = {
userListToSendMessage?: string[];
};
export const useMessagesController = () => {
const navigation = useRouter();
const { id: locationId, workScheduleType } = useAppSelector(
(state) => state.location.adminLocation,
);
const { uid, name, archived, isGroup } = useLocalSearchParams<{
uid: string;
name: string;
archived: string;
isGroup: string;
}>();
const [chatUser, setChatUser] = useState<CometChat.User | undefined>();
const [group, setGroup] = useState<CometChat.Group | undefined>();
const [loading, setLoading] = useState(true);
const [sending, setSending] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [currentUser, setCurrentUser] = useState<CometChat.User | null>(null);
const [messageImage, setMessageImage] = useState<string>();
const [groupUserIds, setGroupUserIds] = useState<string[]>([]);
const cometChatMessageListRef =
useRef<CometChatMessageListActionsInterface>(null);
const { broadcastedUsersData, broadcastedUsersLoading } =
useGetBroadcastedUsers(locationId, groupUserIds);
const goBack = useCallback(() => navigation.back(), [navigation]);
const fetchMessages = async () => {
if (!uid) return;
const currentUser = await CometChat.getLoggedinUser();
setCurrentUser(currentUser);
if (isGroup === 'true') {
const group = await CometChat.getGroup(uid);
setGroup(group);
setLoading(false);
return;
}
const user = await CometChat.getUser(uid);
setChatUser(user);
setLoading(false);
};
useEffect(() => {
fetchMessages();
}, []);
useEffect(() => {
if (!group) return;
const userIds = (group!.getMetadata() as unknown as GroupUsersList)
.userListToSendMessage;
userIds && setGroupUserIds(userIds);
}, [group]);
const handleOpenModal = () => {
setIsVisible(true);
};
const handleCloseModal = (reOpen?: boolean) => {
setIsVisible(false);
if (reOpen) {
const timeout = setTimeout(() => {
handleOpenModal();
clearTimeout(timeout);
}, 100);
}
};
const createMessage = ({
receiverId,
text,
blob,
isUser,
type,
isGroup,
}: {
receiverId: string;
text: string;
blob?: Blob;
isUser?: boolean;
type?: 'audio' | 'image';
isGroup?: boolean;
}) => {
const receiverType = isUser
? CometChat.RECEIVER_TYPE.USER
: CometChat.RECEIVER_TYPE.GROUP;
let message: CometChat.TextMessage | CometChat.MediaMessage =
new CometChat.TextMessage(receiverId, text, receiverType);
if (blob && type) {
message = new CometChat.MediaMessage(
receiverId,
blob,
type === 'image'
? CometChat.MESSAGE_TYPE.IMAGE
: CometChat.MESSAGE_TYPE.AUDIO,
receiverType,
);
if (text) {
message.setCaption(text);
}
}
message.setSender(currentUser!);
message.setSentAt(Math.floor(new Date().getTime() / 1000));
message.setTags([locationId]);
message.setMetadata({
locationId,
broadcastMessage:
(receiverType === CometChat.RECEIVER_TYPE.GROUP || isGroup) ?? false,
});
return message;
};
const sendCometChatMessage = async (
message:
| CometChat.BaseMessage
| CometChat.TextMessage
| CometChat.MediaMessage,
isLastMessage = true,
) => {
const messageRes = await CometChat.sendMessage(message);
isLastMessage && cometChatMessageListRef.current?.addMessage(messageRes);
return messageRes;
};
const handleSendMessage = async (
text: string,
blob?: Blob,
type?: 'audio' | 'image',
) => {
if (!uid || !currentUser) return;
setSending(true);
try {
if (isGroup !== 'true') {
const newMessage = createMessage({
receiverId: uid,
text,
isUser: true,
blob,
type,
});
await sendCometChatMessage(newMessage);
setSending(false);
} else {
const usersListIds = (group!.getMetadata() as unknown as GroupUsersList)
.userListToSendMessage;
if (!usersListIds) return;
const promises = usersListIds.map(async (userId, index) => {
const isLastMessage = index === usersListIds.length - 1;
const message = createMessage({
receiverId: userId,
text,
blob,
isUser: true,
type,
isGroup: true,
});
const messageCopyInGroup = createMessage({
receiverId: uid,
text,
blob,
isUser: false,
type,
});
const sendMessagePromise = await sendCometChatMessage(message);
if (isLastMessage) {
await sendCometChatMessage(messageCopyInGroup, isLastMessage);
}
return sendMessagePromise;
});
await Promise.all(promises);
setSending(false);
}
} catch (e) {
console.log(`Err: ${JSON.stringify(e)}`);
setSending(false);
}
};
const handleSubmitFile = (uri: string, type: 'audio' | 'image') => {
const uriParts = uri.split('.');
const fileType = uriParts[uriParts.length - 1];
let file = undefined;
const data = {
blobId: Date.now(),
offset: 0,
size: 0,
};
if (type === 'audio') {
file = {
name: `audio.${Date.now()}.${fileType}`,
type: 'audio/mp4',
uri,
data,
} as unknown as Blob;
} else {
file = {
uri,
name: `photo.${Date.now()}.${fileType}`,
type: `image/${fileType}`,
data,
} as unknown as Blob;
}
handleSendMessage('', file, type);
};
const onSelectPhoto = () => {
if (Platform.OS === 'ios') {
showActionSheetIOS(handleSubmitFile);
} else {
showActionSheetAndroid(handleSubmitFile);
}
};
const onOpenMessageLink = (url: string) => {
Linking.openURL(url);
};
return {
goBack,
chatUser,
group,
userName: name,
isArchived: archived === 'true',
handleOpenModal,
handleCloseModal,
isVisible,
handleSubmitFile,
handleSendMessage,
onSelectPhoto,
cometChatMessageListRef,
loading: loading || broadcastedUsersLoading,
isGroup: isGroup === 'true',
sending,
messageImage,
setMessageImage,
setShowTooltip,
showTooltip,
onOpenMessageLink,
groupUsers: broadcastedUsersData
? mapWorkforceUsersForMessages(
broadcastedUsersData,
locationId,
workScheduleType as WorkScheduleType[],
)
: [],
};
};
For your information: This is also happening on IOS as well, and its also happening when I put my app in the background and reopen the app again
In CometChatMessageList.tsx
This method is overriding the message on losing focus: getNewMessages
This is happening even if I don't use my custom UI With CometChat default UI it is happening as well
Found the issue, I was not setting muid
when sending a new message and it was being filtered on library side as muid was not defined.
Closing..
Describe the problem
What was the expected behavior?
Reproduction
Please see the attached video: https://we.tl/t-HfW1zmFAcL
Environment