diff --git a/src/components/Chat/MessageDisplay.tsx b/src/components/Chat/MessageDisplay.tsx index 52d0d93..17a57ef 100644 --- a/src/components/Chat/MessageDisplay.tsx +++ b/src/components/Chat/MessageDisplay.tsx @@ -1,28 +1,28 @@ -import React, { useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; import DOMPurify from 'dompurify'; -import './styles.css'; +import './chat.css'; import { executeEvent } from '../../utils/events'; import { Embed } from '../Embeds/Embed'; export const extractComponents = (url) => { - if (!url || !url.startsWith("qortal://")) { + if (!url || !url.startsWith('qortal://')) { return null; } // Skip links starting with "qortal://use-" - if (url.startsWith("qortal://use-")) { + if (url.startsWith('qortal://use-')) { return null; } - url = url.replace(/^(qortal\:\/\/)/, ""); - if (url.includes("/")) { - let parts = url.split("/"); + url = url.replace(/^(qortal\:\/\/)/, ''); + if (url.includes('/')) { + let parts = url.split('/'); const service = parts[0].toUpperCase(); parts.shift(); const name = parts[0]; parts.shift(); let identifier; - const path = parts.join("/"); + const path = parts.join('/'); return { service, name, identifier, path }; } @@ -64,8 +64,7 @@ function processText(input) { } const linkify = (text) => { - if (!text) return ""; // Return an empty string if text is null or undefined - + if (!text) return ''; // Return an empty string if text is null or undefined let textFormatted = text; const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g; textFormatted = text.replace(urlPattern, (url) => { @@ -75,22 +74,66 @@ const linkify = (text) => { return processText(textFormatted); }; - export const MessageDisplay = ({ htmlContent, isReply }) => { - - - const sanitizedContent = useMemo(()=> { + const sanitizedContent = useMemo(() => { return DOMPurify.sanitize(linkify(htmlContent), { ALLOWED_TAGS: [ - 'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img', - 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 's', 'hr' + 'a', + 'b', + 'i', + 'em', + 'strong', + 'p', + 'br', + 'div', + 'span', + 'img', + 'ul', + 'ol', + 'li', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'blockquote', + 'code', + 'pre', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td', + 's', + 'hr', ], ALLOWED_ATTR: [ - 'href', 'target', 'rel', 'class', 'src', 'alt', 'title', - 'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url' + 'href', + 'target', + 'rel', + 'class', + 'src', + 'alt', + 'title', + 'width', + 'height', + 'style', + 'align', + 'valign', + 'colspan', + 'rowspan', + 'border', + 'cellpadding', + 'cellspacing', + 'data-url', ], - }).replace(/]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, ''); - }, [htmlContent]) + }).replace( + /]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, + '' + ); + }, [htmlContent]); const handleClick = async (e) => { e.preventDefault(); @@ -98,7 +141,7 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { const target = e.target; if (target.tagName === 'A') { const href = target.getAttribute('href'); - if(window?.electronAPI){ + if (window?.electronAPI) { window.electronAPI.openExternal(href); } else { window.open(href, '_system'); @@ -106,32 +149,32 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { } else if (target.getAttribute('data-url')) { const url = target.getAttribute('data-url'); - let copyUrl = url + let copyUrl = url; - try { - copyUrl = copyUrl.replace(/^(qortal:\/\/)/, '') - if (copyUrl.startsWith('use-')) { - // Handle the new 'use' format - const parts = copyUrl.split('/') - const type = parts[0].split('-')[1] // e.g., 'group' from 'use-group' - parts.shift() - const action = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., 'invite' from 'action-invite' - parts.shift() - const idPrefix = parts.length > 0 ? parts[0].split('-')[0] : null // e.g., 'groupid' from 'groupid-321' - const id = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., '321' from 'groupid-321' - if(action === 'join'){ - executeEvent("globalActionJoinGroup", { groupId: id}); - return + try { + copyUrl = copyUrl.replace(/^(qortal:\/\/)/, ''); + if (copyUrl.startsWith('use-')) { + // Handle the new 'use' format + const parts = copyUrl.split('/'); + const type = parts[0].split('-')[1]; // e.g., 'group' from 'use-group' + parts.shift(); + const action = parts.length > 0 ? parts[0].split('-')[1] : null; // e.g., 'invite' from 'action-invite' + parts.shift(); + const idPrefix = parts.length > 0 ? parts[0].split('-')[0] : null; // e.g., 'groupid' from 'groupid-321' + const id = parts.length > 0 ? parts[0].split('-')[1] : null; // e.g., '321' from 'groupid-321' + if (action === 'join') { + executeEvent('globalActionJoinGroup', { groupId: id }); + return; + } } + } catch (error) { + //error } - } catch (error) { - //error - } const res = extractComponents(url); if (res) { const { service, name, identifier, path } = res; - executeEvent("addTab", { data: { service, name, identifier, path } }); - executeEvent("open-apps-mode", { }); + executeEvent('addTab', { data: { service, name, identifier, path } }); + executeEvent('open-apps-mode', {}); } } }; @@ -141,19 +184,17 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { let embedData = null; if (embedLink) { - embedData = embedLink[0] + embedData = embedLink[0]; } return ( <> - {embedLink && ( - - )} -
+ {embedLink && } +
); }; diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx index 4d9019c..0bfb5f0 100644 --- a/src/components/Chat/MessageItem.tsx +++ b/src/components/Chat/MessageItem.tsx @@ -1,573 +1,653 @@ -import { Message } from "@chatscope/chat-ui-kit-react"; -import React, { 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 } from "@mui/material"; -import { formatTimestamp } from "../../utils/time"; -import { getBaseApi } from "../../background"; -import { MyContext, 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 { executeEvent } from "../../utils/events"; -import { WrapperUserAction } from "../WrapperUserAction"; -import ReplyIcon from "@mui/icons-material/Reply"; -import { Spacer } from "../../common/Spacer"; -import { ReactionPicker } from "../ReactionPicker"; +import React, { + 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 { MyContext, 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 { addressInfoKeySelector } from "../../atoms/global"; -import { useRecoilValue } from "recoil"; -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 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'; -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 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; } -} -export const MessageItem = React.memo(({ - message, - onSeen, - isLast, - isTemp, - myAddress, - onReply, - isShowingAsReply, - reply, - replyIndex, - scrollToItem, - handleReaction, - reactions, - isUpdating, - lastSignature, - onEdit, - isPrivate -}) => { +}; -const {getIndividualUserInfo} = useContext(MyContext) - const [anchorEl, setAnchorEl] = useState(null); - const [selectedReaction, setSelectedReaction] = useState(null); - const [userInfo, setUserInfo] = useState(null) +export const MessageItem = React.memo( + ({ + message, + onSeen, + isLast, + isTemp, + myAddress, + onReply, + isShowingAsReply, + reply, + replyIndex, + scrollToItem, + handleReaction, + reactions, + isUpdating, + lastSignature, + onEdit, + isPrivate, + }) => { + const { getIndividualUserInfo } = useContext(MyContext); + 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) { - // - } - } + 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]) + getInfo(); + }, [message?.sender, getIndividualUserInfo]); -const htmlText = useMemo(()=> { - - if(message?.messageText){ - return generateHTML(message?.messageText, [ - StarterKit, - Underline, - Highlight, - Mention, - TextStyle - ]) - } - -}, [message?.editTimestamp]) + const htmlText = useMemo(() => { + if (message?.messageText) { + return generateHTML(message?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention, + TextStyle, + ]); + } + }, [message?.editTimestamp]); + const htmlReply = useMemo(() => { + if (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 htmlReply = useMemo(()=> { - - if(reply?.messageText){ - return generateHTML(reply?.messageText, [ - StarterKit, - Underline, - Highlight, - Mention, - TextStyle - ]) - } - -}, [reply?.editTimestamp]) + const onSeenFunc = useCallback(() => { + onSeen(message.id); + }, [message?.id]); -const userAvatarUrl = useMemo(()=> { - return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ - message?.senderName - }/qortal_avatar?async=true` : '' -}, []) + const theme = useTheme(); -const onSeenFunc = useCallback(()=> { - onSeen(message.id); -}, [message?.id]) + return ( + <> + {message?.divide && ( +
+ Unread messages below +
+ )} - - return ( - <> - {message?.divide && ( -
- Unread messages below -
- )} - - - - -
- {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) - - + gap: '7px', + opacity: isTemp || isUpdating ? 0.5 : 1, + padding: '10px', + width: '95%', }} + id={message?.signature} > - - - Replied to {reply?.senderName || reply?.senderAddress} - {reply?.messageText && ( - - )} - {reply?.decryptedData?.type === "notification" ? ( - - ) : ( - - )} - - - - )} - {htmlText && ( - - )} - - - {message?.decryptedData?.type === "notification" ? ( - - ) : ( - - )} - - - {reactions && Object.keys(reactions).map((reaction)=> { - const numberOfReactions = reactions[reaction]?.length - // const myReaction = reactions - 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", - }} - PaperProps={{ - style: { - backgroundColor: "#232428", - color: "white", - }, - }} - > - - - People who reacted with {selectedReaction} - - - {reactions[selectedReaction]?.map((reactionItem) => ( - - - - ))} - - + + {message?.senderName?.charAt(0)} + + + + + - - )} - - {message?.isNotEncrypted && isPrivate && ( - )} - - {isUpdating ? ( - - {message?.status === 'failed-permanent' ? 'Failed to update' : 'Updating...'} - - ) : isTemp ? ( - - {message?.status === 'failed-permanent' ? 'Failed to send' : 'Sending...'} - - ) : ( - <> - {message?.isEdit && ( - - Edited - - )} - - {formatTimestamp(message.timestamp)} - - - )} - -
-
- -
-
- - ); -}); + + + + + {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); + }} + > + + + + Replied to {reply?.senderName || reply?.senderAddress} + + {reply?.messageText && ( + + )} + {reply?.decryptedData?.type === 'notification' ? ( + + ) : ( + + )} + + + + )} + {htmlText && } + {message?.decryptedData?.type === 'notification' ? ( + + ) : ( + + )} -export const ReplyPreview = ({message, isEdit})=> { + + + {reactions && + Object.keys(reactions).map((reaction) => { + const numberOfReactions = reactions[reaction]?.length; + // const myReaction = reactions + 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', + }} + PaperProps={{ + // TODO: deprecated + style: { + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + }, + }} + > + + + People who reacted with {selectedReaction} + + + + {reactions[selectedReaction]?.map((reactionItem) => ( + + + + ))} + + + + + )} + + {message?.isNotEncrypted && isPrivate && ( + + )} + + {isUpdating ? ( + + {message?.status === 'failed-permanent' + ? 'Failed to update' + : 'Updating...'} + + ) : isTemp ? ( + + {message?.status === 'failed-permanent' + ? 'Failed to send' + : 'Sending...'} + + ) : ( + <> + {message?.isEdit && ( + + Edited + + )} + + {formatTimestamp(message.timestamp)} + + + )} + +
+
+
+ + + ); + } +); + +export const ReplyPreview = ({ message, isEdit }) => { + const theme = useTheme(); return ( + + + {isEdit ? ( + - - - {isEdit ? ( - Editing Message - ) : ( - Replied to {message?.senderName || message?.senderAddress} - )} - - {message?.messageText && ( - - )} - {message?.decryptedData?.type === "notification" ? ( - - ) : ( - - )} - - - - ) -} + Editing Message + + ) : ( + + Replied to {message?.senderName || message?.senderAddress} + + )} -const MessageWragger = ({lastMessage, onSeen, isLast, children})=> { + {message?.messageText && ( + + )} - if(lastMessage){ + {message?.decryptedData?.type === 'notification' ? ( + + ) : ( + + )} + + + ); +}; + +const MessageWragger = ({ lastMessage, onSeen, isLast, children }) => { + if (lastMessage) { return ( - {children} - ) + + {children} + + ); } - return children -} + return children; +}; -const WatchComponent = ({onSeen, isLast, children})=> { +const WatchComponent = ({ onSeen, isLast, children }) => { const { ref, inView } = useInView({ threshold: 0.7, // Fully visible triggerOnce: true, // Only trigger once when it becomes visible @@ -577,20 +657,22 @@ const WatchComponent = ({onSeen, isLast, children})=> { useEffect(() => { if (inView && isLast && onSeen) { - - setTimeout(() => { - onSeen(); + setTimeout(() => { + onSeen(); }, 100); - } }, [inView, isLast, onSeen]); - return
- {children} -
- -} \ No newline at end of file + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/Chat/styles.css b/src/components/Chat/chat.css similarity index 83% rename from src/components/Chat/styles.css rename to src/components/Chat/chat.css index 3c7c570..61e0189 100644 --- a/src/components/Chat/styles.css +++ b/src/components/Chat/chat.css @@ -1,6 +1,6 @@ .tiptap { margin-top: 0; - color: white; /* Set default font color to white */ + color: ''; /* Set default font color to white */ width: 100%; } @@ -105,46 +105,45 @@ color: white; /* Ensure paragraph text color is white */ margin: 0px; } - .tiptap p.is-editor-empty:first-child::before { - color: #adb5bd; - content: attr(data-placeholder); - float: left; - height: 0; - pointer-events: none; - } +.tiptap p.is-editor-empty:first-child::before { + color: #adb5bd; + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; +} - .tiptap p:empty::before { - content: ''; - display: inline-block; - } +.tiptap p:empty::before { + content: ''; + display: inline-block; +} .tiptap a { - color: cadetblue + color: cadetblue; } .tiptap img { display: block; - max-width: 100%; + max-width: 100%; } .isReply p { font-size: 12px !important; } -.tiptap [data-type="mention"] { +.tiptap [data-type='mention'] { box-decoration-break: clone; color: lightblue; padding: 0.1rem 0.3rem; } - .unread-divider { width: 90%; - color: white; - border-bottom: 1px solid white; - display: flex; - justify-content: center; - border-radius: 2px; + color: white; + border-bottom: 1px solid white; + display: flex; + justify-content: center; + border-radius: 2px; } .mention-item { @@ -176,4 +175,4 @@ background-color: gray; } } -} \ No newline at end of file +}