cometchat / cometchat-uikit-react-native

Ready-to-use Chat UI Components for React Native
https://www.cometchat.com
Other
40 stars 24 forks source link

CometChatList Connections Listeners getting disconnected when custom components are being used #67

Closed jasimawan closed 2 months ago

jasimawan commented 2 months ago

Describe the problem

In my messages screen I am using CometChatMessageList, and I have my own Custom Message Composer. Whenever I try to send an image or voice message from my custom composer it the listeners in CometChatList.tsx gets disconnected and it resets the whole list of messages and only show one message and rest is disappeared

What was the expected behavior?

It should not reset the messages

Reproduction

Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent. Note: If clear, reproducable steps or the smallest sample app demonstrating misbehavior cannot be provided, we may not be able to follow up on this bug report.

Where applicable, please include:

  • The smallest possible sample app that reproduces the undesirable behavior
  • Log files (redact/remove sensitive information)
  • Application settings (redact/remove sensitive information)
  • Screenshots

Please see the attached video: https://we.tl/t-HfW1zmFAcL

Environment

Please provide the following:

cometchat-helpcenter-bot commented 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

jasimawan commented 2 months ago

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[],
        )
      : [],
  };
};
jasimawan commented 2 months ago

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

jasimawan commented 2 months ago

In CometChatMessageList.tsx This method is overriding the message on losing focus: getNewMessages

jasimawan commented 2 months ago

This is happening even if I don't use my custom UI With CometChat default UI it is happening as well

jasimawan commented 2 months ago

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..