import {
memo,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { useInView } from 'react-intersection-observer';
import { MessageDisplay } from './MessageDisplay';
import {
Avatar,
Box,
Button,
ButtonBase,
List,
ListItem,
ListItemText,
Popover,
Tooltip,
Typography,
useTheme,
} from '@mui/material';
import { formatTimestamp } from '../../utils/time';
import { QORTAL_APP_CONTEXT, getBaseApiReact } from '../../App';
import { generateHTML } from '@tiptap/react';
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 { WrapperUserAction } from '../WrapperUserAction';
import ReplyIcon from '@mui/icons-material/Reply';
import { Spacer } from '../../common/Spacer';
import { ReactionPicker } from '../ReactionPicker';
import KeyOffIcon from '@mui/icons-material/KeyOff';
import EditIcon from '@mui/icons-material/Edit';
import TextStyle from '@tiptap/extension-text-style';
import level0Img from '../../assets/badges/level-0.png';
import level1Img from '../../assets/badges/level-1.png';
import level2Img from '../../assets/badges/level-2.png';
import level3Img from '../../assets/badges/level-3.png';
import level4Img from '../../assets/badges/level-4.png';
import level5Img from '../../assets/badges/level-5.png';
import level6Img from '../../assets/badges/level-6.png';
import level7Img from '../../assets/badges/level-7.png';
import level8Img from '../../assets/badges/level-8.png';
import level9Img from '../../assets/badges/level-9.png';
import level10Img from '../../assets/badges/level-10.png';
import { Embed } from '../Embeds/Embed';
import {
buildImageEmbedLink,
isHtmlString,
messageHasImage,
} from '../../utils/chat';
import { useTranslation } from 'react-i18next';
const getBadgeImg = (level) => {
switch (level?.toString()) {
case '0':
return level0Img;
case '1':
return level1Img;
case '2':
return level2Img;
case '3':
return level3Img;
case '4':
return level4Img;
case '5':
return level5Img;
case '6':
return level6Img;
case '7':
return level7Img;
case '8':
return level8Img;
case '9':
return level9Img;
case '10':
return level10Img;
default:
return level0Img;
}
};
const UserBadge = memo(({ userInfo }) => {
return (
);
});
export const MessageItem = memo(
({
handleReaction,
isLast,
isPrivate,
isShowingAsReply,
isTemp,
isUpdating,
lastSignature,
message,
myAddress,
onEdit,
onReply,
onSeen,
reactions,
reply,
replyIndex,
scrollToItem,
}) => {
const { getIndividualUserInfo } = useContext(QORTAL_APP_CONTEXT);
const [anchorEl, setAnchorEl] = useState(null);
const [selectedReaction, setSelectedReaction] = useState(null);
const [userInfo, setUserInfo] = useState(null);
useEffect(() => {
const getInfo = async () => {
if (!message?.sender) return;
try {
const res = await getIndividualUserInfo(message?.sender);
if (!res) return null;
setUserInfo(res);
} catch (error) {
//
}
};
getInfo();
}, [message?.sender, getIndividualUserInfo]);
const htmlText = useMemo(() => {
if (message?.messageText) {
const isHtml = isHtmlString(message?.messageText);
if (isHtml) return message?.messageText;
return generateHTML(message?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle,
]);
}
}, [message?.editTimestamp]);
const htmlReply = useMemo(() => {
if (reply?.messageText) {
const isHtml = isHtmlString(reply?.messageText);
if (isHtml) return reply?.messageText;
return generateHTML(reply?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle,
]);
}
}, [reply?.editTimestamp]);
const userAvatarUrl = useMemo(() => {
return message?.senderName
? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
message?.senderName
}/qortal_avatar?async=true`
: '';
}, []);
const onSeenFunc = useCallback(() => {
onSeen(message.id);
}, [message?.id]);
const theme = useTheme();
const { t } = useTranslation([
'auth',
'core',
'group',
'question',
'tutorial',
]);
return (
<>
{message?.divide && (
{t('core:message.generic.unread_messages', {
postProcess: 'capitalizeFirstChar',
})}
)}
{isShowingAsReply ? (
) : (
{message?.senderName?.charAt(0)}
)}
{message?.senderName || message?.sender}
{message?.sender === myAddress &&
(!message?.isNotEncrypted || isPrivate === false) && (
{
onEdit(message);
}}
>
)}
{!isShowingAsReply && (
{
onReply(message);
}}
>
)}
{!isShowingAsReply && handleReaction && (
{
if (
reactions &&
reactions[val] &&
reactions[val]?.find(
(item) => item?.sender === myAddress
)
) {
handleReaction(val, message, false);
} else {
handleReaction(val, message, true);
}
}}
/>
)}
{reply && (
<>
{
scrollToItem(replyIndex);
}}
>
{t('core:message.generic.replied_to', {
person: reply?.senderName || reply?.senderAddress,
postProcess: 'capitalizeFirstChar',
})}
{reply?.messageText && (
)}
{reply?.decryptedData?.type === 'notification' ? (
) : (
)}
>
)}
{htmlText && }
{message?.decryptedData?.type === 'notification' ? (
) : (
)}
{message?.images && messageHasImage(message) && (
)}
{reactions &&
Object.keys(reactions).map((reaction) => {
const numberOfReactions = reactions[reaction]?.length;
if (numberOfReactions === 0) return null;
return (
{
event.stopPropagation(); // Prevent event bubbling
setAnchorEl(event.currentTarget);
setSelectedReaction(reaction);
}}
>
{reaction}
{' '}
{numberOfReactions > 1 && (
{numberOfReactions}
)}
);
})}
{selectedReaction && (
{
setAnchorEl(null);
setSelectedReaction(null);
}}
anchorOrigin={{
vertical: 'top',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
slotProps={{
paper: {
style: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
},
},
}}
>
{t('core:message.generic.people_reaction', {
reaction: selectedReaction,
postProcess: 'capitalizeFirstChar',
})}
{reactions[selectedReaction]?.map((reactionItem) => (
))}
)}
{message?.isNotEncrypted && isPrivate && (
)}
{isUpdating ? (
{message?.status === 'failed-permanent'
? t('core:message.error.update_failed', {
postProcess: 'capitalizeFirstChar',
})
: t('core:message.generic.updating', {
postProcess: 'capitalizeFirstChar',
})}
) : isTemp ? (
{message?.status === 'failed-permanent'
? t('core:message.error.send_failed', {
postProcess: 'capitalizeFirstChar',
})
: t('core:message.generic.sending', {
postProcess: 'capitalizeFirstChar',
})}
) : (
<>
{message?.isEdit && (
{t('core:message.generic.edited', {
postProcess: 'capitalizeFirstChar',
})}
)}
{formatTimestamp(message.timestamp)}
>
)}
>
);
}
);
export const ReplyPreview = ({ message, isEdit = false }) => {
const theme = useTheme();
const { t } = useTranslation([
'auth',
'core',
'group',
'question',
'tutorial',
]);
const replyMessageText = useMemo(() => {
const isHtml = isHtmlString(message?.messageText);
if (isHtml) return message?.messageText;
return generateHTML(message?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle,
]);
}, [message?.messageText]);
return (
{isEdit ? (
{t('core:message.generic.editing_message', {
postProcess: 'capitalizeFirstChar',
})}
) : (
{t('core:message.generic.replied_to', {
person: message?.senderName || message?.senderAddress,
postProcess: 'capitalizeFirstChar',
})}
)}
{message?.messageText && (
)}
{message?.decryptedData?.type === 'notification' ? (
) : (
)}
);
};
const MessageWragger = ({ lastMessage, onSeen, isLast, children }) => {
if (lastMessage) {
return (
{children}
);
}
return children;
};
const WatchComponent = ({ onSeen, isLast, children }) => {
const { ref, inView } = useInView({
threshold: 0.7, // Fully visible
triggerOnce: true, // Only trigger once when it becomes visible
delay: 100,
trackVisibility: false,
});
useEffect(() => {
if (inView && isLast && onSeen) {
setTimeout(() => {
onSeen();
}, 100);
}
}, [inView, isLast, onSeen]);
return (
{children}
);
};