import React, { useEffect, useRef, useState, useMemo } from 'react';
import { useInView } from 'react-intersection-observer';
import {
HMSMessage,
selectMessagesByPeerID,
selectMessagesByRole,
selectPeerNameByID,
selectBroadcastMessages,
selectBroadcastMessagesUnreadCount,
selectMessagesUnreadCountByRole,
selectMessagesUnreadCountByPeerID,
} from '@100mslive/hms-video-store';
import { CloseIcon, DownCaratIcon, PeopleIcon, SendIcon } from '../Icons';
import { Button } from '../Button';
import { ChatSelector } from './ChatSelector';
import { useHMSTheme } from '../../hooks/HMSThemeProvider';
import { useHMSActions, useHMSStore } from '../../hooks/HMSRoomProvider';
import { hmsUiClassParserGenerator } from '../../utils/classes';
import { ChatLink, scrollToBottom } from './chatBoxUtils';
import './index.css';
interface ChatBoxClasses {
root?: string;
header?: string;
headerLine?: string;
headerRoot?: string;
headerText?: string;
headerCloseButton?: string;
messageBox?: string;
messageRoot?: string;
messageInfo?: string;
messageSender?: string;
messageTime?: string;
messageText?: string;
notificationRoot?: string;
notificationInfo?: string;
notificationText?: string;
notificationTime?: string;
time?: string;
noMessageRoot?: string;
footer?: string;
chatInput?: string;
unreadMessagesContainer?: string;
unreadMessagesInner?: string;
unreadIcon?: string;
}
const defaultClasses: ChatBoxClasses = {
root: 'w-full h-full rounded-2xl flex flex-col shadow-2 relative',
header: `bg-white dark:bg-gray-200 rounded-t-2xl p-3 text-gray-300 dark:text-gray-500 flex flex-col justify-center items-center shadow border-b-1 border-gray-500 cursor-pointer`,
headerLine: 'w-8 h-1 rounded bg-white dark:bg-gray-400 m-2',
headerRoot: 'flex w-full justify-between',
headerText: 'text-gray-300 dark:text-gray-500 flex items-center',
headerCloseButton: 'focus:outline-none',
messageBox:
'bg-white dark:bg-gray-100 w-full h-0 p-3 text-gray-300 dark:text-gray-500 overflow-y-auto no-scrollbar flex-auto',
messageRoot: 'py-3',
messageInfo: 'flex justify-between',
messageTime: 'text-xs',
messageText: 'text-gray-100 dark:text-white leading-5 max-w-full break-words',
noMessageRoot:
'flex justify-center items-center text-gray-300 dark:text-gray-400 h-full',
footer:
'bg-white dark:bg-gray-200 min-h-11 rounded-b-2xl flex w-full justify-between p-3 border-t-1 border-gray-500 relative',
chatInput:
'bg-white dark:bg-gray-200 placeholder-gray-500 text-gray-100 dark:text-white focus:outline-none leading-5 overflow-y-auto no-scrollbar resize-none w-5/6',
notificationRoot: 'py-3',
notificationInfo: 'flex justify-between text-gray-300 dark:text-gray-400',
notificationTime: 'text-xs',
unreadMessagesContainer:
'absolute left-0 p-1 w-full bottom-full flex justify-center',
unreadMessagesInner:
'rounded-md px-2 py-1 bg-brand-main text-white flex cursor-pointer items-center ',
unreadIcon: 'ml-2 w-3 h-3',
};
const customClasses: ChatBoxClasses = {
messageBox: 'hmsui-chatBox-no-scrollbar',
chatInput: 'hmsui-chatBox-no-scrollbar',
};
export interface Message extends HMSMessage {
notification?: boolean;
direction?: 'left' | 'right' | 'center';
}
export interface ChatProps {
messages?: Message[];
onSend?: (message: string) => void;
onClose?: () => void; // when the chat box is closed
autoScrollToBottom?: boolean;
scrollAnimation?: ScrollBehavior;
messageFormatter?: (message: string, receiver?: string) => React.ReactNode;
/**
* extra classes added by user
*/
classes?: ChatBoxClasses;
timeFormatter?: (date: Date) => string;
}
export const ChatBox = ({
messages,
onSend,
onClose,
autoScrollToBottom = true, //TODO shouldn't be exposed as a prop
scrollAnimation = 'auto', //TODO shouldn't be exposed as a prop
messageFormatter = (message: string) => {
return (
);
},
classes,
timeFormatter = (date: Date) => {
const min = date.getMinutes();
const minString = min < 10 ? `0${min}` : min;
return `${date.getHours()}:${minString}`;
},
}: ChatProps) => {
const { tw, toast } = useHMSTheme();
const styler = useMemo(
() =>
hmsUiClassParserGenerator({
tw,
classes,
customClasses,
defaultClasses,
tag: 'hmsui-chatBox',
}),
[],
);
const hmsActions = useHMSActions();
const [selection, setSelection] = useState({ role: '', peerId: '' });
const [showChatSelection, setShowChatSelection] = useState(false);
const selectedPeerName = useHMSStore(selectPeerNameByID(selection.peerId));
const storeMessageSelector = selection.role
? selectMessagesByRole(selection.role)
: selection.peerId
? selectMessagesByPeerID(selection.peerId)
: selectBroadcastMessages;
const storeUnreadMessageCountSelector = selection.role
? selectMessagesUnreadCountByRole(selection.role)
: selection.peerId
? selectMessagesUnreadCountByPeerID(selection.peerId)
: selectBroadcastMessagesUnreadCount;
const storeMessages = useHMSStore(storeMessageSelector) || [];
const unreadCount = useHMSStore(storeUnreadMessageCountSelector);
messages = messages || storeMessages;
const sendMessage = async (message: string) => {
if (onSend) {
onSend(message);
return;
}
try {
if (selection.role) {
await hmsActions.sendGroupMessage(message, [selection.role]);
} else if (selection.peerId) {
await hmsActions.sendDirectMessage(message, selection.peerId);
} else {
await hmsActions.sendBroadcastMessage(message);
}
} catch (error) {
toast((error as Error).message);
}
};
const [messageDraft, setMessageDraft] = useState('');
// a dummy element with messagesEndRef is created and put in the end
const { ref: messagesEndRef, inView: messagesEndInView } = useInView();
const messageListRef = useRef(null);
/** Effect to scroll to bottom when chat is opened */
useEffect(() => {
if (messageListRef.current) {
scrollToBottom(messageListRef, scrollAnimation);
}
}, []); //eslint-disable-line
useEffect(() => {
if (messages && messages.length > 0) {
const myOwnMessage = messages[messages.length - 1].senderName === 'You';
if (autoScrollToBottom && (myOwnMessage || messagesEndInView)) {
scrollToBottom(messageListRef, scrollAnimation);
hmsActions.setMessageRead(true);
}
}
}, [messages]);
useEffect(() => {
if (messagesEndInView && unreadCount > 0) {
// Mark only crrent view messages as read
messages?.forEach(message => {
hmsActions.setMessageRead(true, message.id);
});
}
}, [
selection.role,
selection.peerId,
messages,
messagesEndInView,
unreadCount,
]);
return (
{/* root */}
{/* header */}
{/* header-line */}
{/*
*/}
{/* header-root */}
{/* header-text */}
setShowChatSelection(value => !value)}
>
{selectedPeerName || selection.role || 'Everyone'}
{
setSelection({ role: role, peerId: peer });
setShowChatSelection(false);
}}
/>
{/* messageBox */}
{/* TODO: move no scroll bar css logic to tailwind */}
{messages.map(message => {
return message.notification ? (
/* notificationRoot */
{/* notificationInfo*/}
{/*notificationText*/}
{messageFormatter
? messageFormatter(message.message)
: message.message}
{timeFormatter(message.time)}
) : (
/* messageRoot */
{/* messageInfo */}
{/* messageSender */}
{message.senderName}
{/* messageTime */}
{timeFormatter(message.time)}
{/* messageText */}
{messageFormatter
? messageFormatter(message.message)
: message.message}
);
})}
{messages.length === 0 && (
/* NoMessageRoot */
There are no messages here.
)}
{/* footer */}
{unreadCount !== 0 && (
{
scrollToBottom(messageListRef, scrollAnimation);
}}
>
{`New message${unreadCount > 1 ? 's' : ''}`}
)}
{/* chatInput */}
{/* TODO: move no scrollbar logic to tailwind */}
);
};