import { Avatar, Box, ButtonBase, InputBase, MenuItem, Select, Typography, Tooltip, useTheme, } from '@mui/material'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import SearchIcon from '@mui/icons-material/Search'; import { Spacer } from '../../common/Spacer'; import AlternateEmailIcon from '@mui/icons-material/AlternateEmail'; import CloseIcon from '@mui/icons-material/Close'; import InsertLinkIcon from '@mui/icons-material/InsertLink'; import Highlight from '@tiptap/extension-highlight'; import Mention from '@tiptap/extension-mention'; import StarterKit from '@tiptap/starter-kit'; import Underline from '@tiptap/extension-underline'; import { AppsSearchContainer, AppsSearchLeft, AppsSearchRight, } from '../Apps/Apps-styles'; import IconClearInput from '../../assets/svgs/ClearInput.svg'; import { CellMeasurerCache } from 'react-virtualized'; import { getBaseApiReact } from '../../App'; import { MessageDisplay } from './MessageDisplay'; import { useVirtualizer } from '@tanstack/react-virtual'; import { formatTimestamp } from '../../utils/time'; import { ContextMenuMentions } from '../ContextMenuMentions'; import { convert } from 'html-to-text'; import { generateHTML } from '@tiptap/react'; import ErrorBoundary from '../../common/ErrorBoundary'; const extractTextFromHTML = (htmlString = '') => { return convert(htmlString, { wordwrap: false, // Disable word wrapping })?.toLowerCase(); }; const cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 50, }); export const ChatOptions = ({ messages: untransformedMessages, goToMessage, members, myName, selectedGroup, openQManager, isPrivate, }) => { const [mode, setMode] = useState('default'); const [searchValue, setSearchValue] = useState(''); const [selectedMember, setSelectedMember] = useState(0); const theme = useTheme(); const { t } = useTranslation(['auth', 'core', 'group']); const parentRef = useRef(null); const parentRefMentions = useRef(null); const [lastMentionTimestamp, setLastMentionTimestamp] = useState(null); const [debouncedValue, setDebouncedValue] = useState(''); // Debounced value const messages = useMemo(() => { return untransformedMessages?.map((item) => { if (item?.messageText) { let transformedMessage = item?.messageText; try { transformedMessage = generateHTML(item?.messageText, [ StarterKit, Underline, Highlight, Mention, ]); return { ...item, messageText: transformedMessage, }; } catch (error) { console.log(error); } } else return item; }); }, [untransformedMessages]); const getTimestampMention = async () => { try { return new Promise((res, rej) => { window .sendMessage('getTimestampMention') .then((response) => { if (!response?.error) { if (response && selectedGroup && response[selectedGroup]) { setLastMentionTimestamp(response[selectedGroup]); } res(response); return; } rej(response.error); }) .catch((error) => { rej( error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirst', }) ); }); }); } catch (error) { console.log(error); } }; useEffect(() => { if (mode === 'mentions' && selectedGroup) { window .sendMessage('addTimestampMention', { timestamp: Date.now(), groupId: selectedGroup, }) .then((res) => { getTimestampMention(); }) .catch((error) => { console.error( 'Failed to add timestamp:', error.message || 'An error occurred' ); }); } }, [mode, selectedGroup]); useEffect(() => { getTimestampMention(); }, []); // Debounce logic useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(searchValue); }, 350); // Cleanup timeout if searchValue changes before the timeout completes return () => { clearTimeout(handler); }; }, [searchValue]); // Runs effect when searchValue changes const searchedList = useMemo(() => { if (!debouncedValue?.trim()) { if (selectedMember) { return messages .filter((message) => message?.senderName === selectedMember) ?.sort((a, b) => b?.timestamp - a?.timestamp); } return []; } if (selectedMember) { return messages .filter( (message) => message?.senderName === selectedMember && extractTextFromHTML( isPrivate ? message?.messageText : message?.decryptedData?.message )?.includes(debouncedValue.toLowerCase()) ) ?.sort((a, b) => b?.timestamp - a?.timestamp); } return messages .filter((message) => extractTextFromHTML( isPrivate === false ? message?.messageText : message?.decryptedData?.message )?.includes(debouncedValue.toLowerCase()) ) ?.sort((a, b) => b?.timestamp - a?.timestamp); }, [debouncedValue, messages, selectedMember, isPrivate]); const mentionList = useMemo(() => { if (!messages || messages.length === 0 || !myName) return []; if (isPrivate === false) { return messages .filter((message) => extractTextFromHTML(message?.messageText)?.includes( `@${myName?.toLowerCase()}` ) ) ?.sort((a, b) => b?.timestamp - a?.timestamp); } return messages .filter((message) => extractTextFromHTML(message?.decryptedData?.message)?.includes( `@${myName?.toLowerCase()}` ) ) ?.sort((a, b) => b?.timestamp - a?.timestamp); }, [messages, myName, isPrivate]); const rowVirtualizer = useVirtualizer({ count: searchedList.length, getItemKey: React.useCallback( (index) => searchedList[index].signature, [searchedList] ), getScrollElement: () => parentRef.current, estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed overscan: 10, // Number of items to render outside the visible area to improve smoothness }); const rowVirtualizerMentions = useVirtualizer({ count: mentionList.length, getItemKey: React.useCallback( (index) => mentionList[index].signature, [mentionList] ), getScrollElement: () => parentRefMentions.current, estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed overscan: 10, // Number of items to render outside the visible area to improve smoothness }); if (mode === 'mentions') { return ( { setMode('default'); }} sx={{ cursor: 'pointer', color: theme.palette.text.primary, }} /> {mentionList?.length === 0 && ( {t('core:message.generic.no_results', { postProcess: 'capitalizeFirst', })} )}
{rowVirtualizerMentions .getVirtualItems() .map((virtualRow) => { const index = virtualRow.index; let message = mentionList[index]; return (
); })}
); } if (mode === 'search') { return ( { setMode('default'); }} sx={{ cursor: 'pointer', color: theme.palette.text.primary, }} /> setSearchValue(e.target.value)} sx={{ ml: 1, flex: 1 }} placeholder="Search chat text" inputProps={{ 'aria-label': 'Search for apps', fontSize: '16px', fontWeight: 400, }} /> {searchValue && ( { setSearchValue(''); }} > )} {!!selectedMember && ( { setSelectedMember(0); }} sx={{ cursor: 'pointer', color: theme.palette.text.primary, }} /> )} {debouncedValue && searchedList?.length === 0 && ( {t('core:message.generic.no_results', { postProcess: 'capitalizeFirst', })} )}
{rowVirtualizer.getVirtualItems().map((virtualRow) => { const index = virtualRow.index; let message = searchedList[index]; return (
{t('group.message.generic.invalid_data', { postProcess: 'capitalizeFirst', })} } >
); })}
); } return ( { setMode('search'); }} > {t('core:action.search', { postProcess: 'capitalizeAll' })} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.default, }, }, arrow: { sx: { color: theme.palette.text.secondary, }, }, }} > { setMode('default'); setSearchValue(''); setSelectedMember(0); openQManager(); }} > {t('core:q_apps.q_manager', { postProcess: 'capitalizeAll' })} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.default, }, }, arrow: { sx: { color: theme.palette.text.secondary, }, }, }} > { setMode('mentions'); setSearchValue(''); setSelectedMember(0); }} > {t('core:message.generic.mentioned', { postProcess: 'capitalizeAll', })} } placement="left" arrow sx={{ fontSize: '24' }} slotProps={{ tooltip: { sx: { color: theme.palette.text.primary, backgroundColor: theme.palette.background.default, }, }, arrow: { sx: { color: theme.palette.text.secondary, }, }, }} > 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? theme.palette.other.unread : theme.palette.text.primary, }} /> ); }; const ShowMessage = ({ message, goToMessage, messages }) => { const theme = useTheme(); return ( {message?.senderName?.charAt(0)} {message?.senderName} {formatTimestamp(message.timestamp)} { const findMsgIndex = messages.findIndex( (item) => item?.signature === message?.signature ); if (findMsgIndex !== -1) { goToMessage(findMsgIndex); } }} > {message?.messageText && ( )} {message?.decryptedData?.message && (

'} /> )}
); };