import React, { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useMedia } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; import { VariableSizeList } from 'react-window'; import { HMSMessage, HMSPeerID, HMSRoleName, selectHMSMessages, selectLocalPeerID, selectLocalPeerName, selectLocalPeerRoleName, selectPeerNameByID, selectSessionStore, selectUnreadHMSMessagesCount, useHMSActions, useHMSStore, useHMSVanillaStore, } from '@100mslive/react-sdk'; import { SolidPinIcon } from '@100mslive/react-icons'; import { Box, Flex } from '../../../Layout'; import { Text } from '../../../Text'; import { config as cssConfig, styled } from '../../../Theme'; import { Tooltip } from '../../../Tooltip'; import { ChatActions } from './ChatActions'; import { EmptyChat } from './EmptyChat'; import { useRoomLayoutConferencingScreen } from '../../provider/roomLayoutProvider/hooks/useRoomLayoutScreen'; // @ts-ignore: No implicit Any import { useSetSubscribedChatSelector } from '../AppData/useUISettings'; import { usePinnedBy } from '../hooks/usePinnedBy'; import { formatTime } from './utils'; import { CHAT_SELECTOR, SESSION_STORE_KEY } from '../../common/constants'; const rowHeights: Record = {}; let listInstance: VariableSizeList | null = null; //eslint-disable-line function getRowHeight(index: number) { // 72 will be default row height for any message length return rowHeights[index]?.size || 72; } const setRowHeight = (index: number, id: string, size: number) => { if (rowHeights[index]?.id === id && rowHeights[index]?.size === size) { return; } listInstance?.resetAfterIndex(Math.max(index - 1, 0)); Object.assign(rowHeights, { [index]: { size, id } }); }; const getMessageBackgroundColor = ( messageType: string, selectedPeerID: string, selectedRole: string, isOverlay: boolean, ) => { if (messageType && !(selectedPeerID || selectedRole)) { return isOverlay ? 'rgba(0, 0, 0, 0.64)' : '$surface_default'; } return ''; }; const MessageTypeContainer = ({ left, right }: { left?: string; right?: string }) => { return ( {left && ( {left} )} {right && ( {right} )} ); }; export const MessageType = ({ roles, hasCurrentUserSent, receiver, }: { roles?: HMSRoleName[]; hasCurrentUserSent: boolean; receiver?: HMSPeerID; }) => { const peerName = useHMSStore(selectPeerNameByID(receiver)); const localPeerRoleName = useHMSStore(selectLocalPeerRoleName); if (receiver) { return ( ); } if (roles && roles.length) { return ; } return null; }; const URL_REGEX = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/; const Link = styled('a', { color: '$primary_default', wordBreak: 'break-word', '&:hover': { textDecoration: 'underline', }, }); export const AnnotisedMessage = ({ message, length }: { message: string; length?: number }) => { if (!message) { return ; } return ( {message .trim() .split(/(\s)/) .map(part => URL_REGEX.test(part) ? ( {part.slice(0, length)} ) : ( part.slice(0, length) ), )} ); }; const getMessageType = ({ roles, receiver }: { roles?: HMSRoleName[]; receiver?: HMSPeerID }) => { if (roles && roles.length > 0) { return 'role'; } return receiver ? 'private' : ''; }; export const SenderName = styled(Text, { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', width: '100%', minWidth: 0, color: '$on_surface_high', fontWeight: '$semiBold', }); const ChatMessage = React.memo( ({ index, style = {}, message }: { message: HMSMessage; index: number; style: React.CSSProperties }) => { const { elements } = useRoomLayoutConferencingScreen(); const rowRef = useRef(null); const isMobile = useMedia(cssConfig.media.md); const isPrivateChatEnabled = !!elements?.chat?.private_chat_enabled; const isOverlay = elements?.chat?.is_overlay && isMobile; const localPeerId = useHMSStore(selectLocalPeerID); const [selectedRole, setRoleSelector] = useSetSubscribedChatSelector(CHAT_SELECTOR.ROLE); const [selectedPeer, setPeerSelector] = useSetSubscribedChatSelector(CHAT_SELECTOR.PEER); const messageType = getMessageType({ roles: message.recipientRoles, receiver: message.recipientPeer, }); const [openSheet, setOpenSheetBare] = useState(false); const showPinAction = !!elements?.chat?.allow_pinning_messages; const showReply = message.sender !== selectedPeer.id && message.sender !== localPeerId && isPrivateChatEnabled; useLayoutEffect(() => { if (rowRef.current) { setRowHeight(index, message.id, rowRef.current.clientHeight); } }, [index, message.id]); const setOpenSheet = (value: boolean, e?: React.MouseEvent) => { e?.stopPropagation(); setOpenSheetBare(value); }; return ( { if (isMobile) { setOpenSheet(true, e); } }} > {message.senderName === 'You' || !message.senderName ? ( {message.senderName || 'Anonymous'} ) : ( {message.sender === localPeerId ? `${message.senderName} (You)` : message.senderName} )} {!isOverlay ? ( {formatTime(message.time)} ) : null} { setRoleSelector(''); setPeerSelector({ id: message.sender, name: message.senderName }); }} onReplyGroup={() => { if (message.senderRole) { setRoleSelector(message.senderRole); setPeerSelector({}); } }} showReply={showReply} isMobile={isMobile} openSheet={openSheet} setOpenSheet={setOpenSheet} /> { setOpenSheet(true, e); }} > ); }, ); const MessageWrapper = React.memo( ({ index, style, data }: { index: number; style: React.CSSProperties; data: HMSMessage[] }) => { return ; }, ); const VirtualizedChatMessages = React.forwardRef< VariableSizeList, { messages: HMSMessage[]; scrollToBottom: (count: number) => void } >(({ messages, scrollToBottom }, listRef) => { const hmsActions = useHMSActions(); const itemKey = useCallback((index: number, data: HMSMessage[]) => { return data[index].id; }, []); useEffect(() => { requestAnimationFrame(() => scrollToBottom(1)); }, [scrollToBottom]); return ( {({ height, width }: { height: number; width: number }) => ( { if (node) { // @ts-ignore listRef.current = node; listInstance = node; } }} itemCount={messages.length} itemSize={getRowHeight} itemData={messages} width={width} height={height} style={{ overflowX: 'hidden', }} itemKey={itemKey} onItemsRendered={({ visibleStartIndex, visibleStopIndex }) => { for (let i = visibleStartIndex; i <= visibleStopIndex; i++) { if (!messages[i].read) { hmsActions.setMessageRead(true, messages[i].id); } } }} > {MessageWrapper} )} ); }); export const ChatBody = React.forwardRef void }>( ({ scrollToBottom }: { scrollToBottom: (count: number) => void }, listRef) => { const messages = useHMSStore(selectHMSMessages); const blacklistedMessageIDs = useHMSStore(selectSessionStore(SESSION_STORE_KEY.CHAT_MESSAGE_BLACKLIST)); const filteredMessages = useMemo(() => { const blacklistedMessageIDSet = new Set(blacklistedMessageIDs || []); return messages?.filter(message => message.type === 'chat' && !blacklistedMessageIDSet.has(message.id)) || []; }, [blacklistedMessageIDs, messages]); const vanillaStore = useHMSVanillaStore(); const rerenderOnFirstMount = useRef(false); useEffect(() => { const unsubscribe = vanillaStore.subscribe(() => { // @ts-ignore if (!listRef.current) { return; } // @ts-ignore const outerElement = listRef.current._outerRef; if ( outerElement && outerElement.clientHeight + outerElement.scrollTop + outerElement.offsetTop >= outerElement.scrollHeight ) { requestAnimationFrame(() => scrollToBottom(1)); } }, selectUnreadHMSMessagesCount); return unsubscribe; }, [vanillaStore, listRef, scrollToBottom]); useEffect(() => { // @ts-ignore if (filteredMessages.length > 0 && listRef?.current && !rerenderOnFirstMount.current) { rerenderOnFirstMount.current = true; // @ts-ignore listRef.current.resetAfterIndex(0); } }, [listRef, filteredMessages]); return filteredMessages.length === 0 ? ( ) : ( ); }, ); const PinnedBy = ({ messageId, index, rowRef, }: { messageId: string; index: number; rowRef?: React.MutableRefObject; }) => { const pinnedBy = usePinnedBy(messageId); const localPeerName = useHMSStore(selectLocalPeerName); useLayoutEffect(() => { if (rowRef?.current) { if (pinnedBy) { rowRef.current.style.background = 'linear-gradient(277deg, var(--hms-ui-colors-surface_default) 0%, var(--hms-ui-colors-surface_dim) 60.87%)'; } else { rowRef.current.style.background = ''; } setRowHeight(index, messageId, rowRef?.current.clientHeight); } }, [index, messageId, pinnedBy, rowRef]); if (!pinnedBy) { return null; } return ( Pinned by {localPeerName === pinnedBy ? 'you' : pinnedBy} ); };