Rename style file and add theme to chat

This commit is contained in:
Nicola Benaglia 2025-04-18 19:39:07 +02:00
parent 2013561db7
commit 69ff35b776
3 changed files with 741 additions and 619 deletions

View File

@ -1,28 +1,28 @@
import React, { useEffect, useMemo } from 'react'; import { useMemo } from 'react';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import './styles.css'; import './chat.css';
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
import { Embed } from '../Embeds/Embed'; import { Embed } from '../Embeds/Embed';
export const extractComponents = (url) => { export const extractComponents = (url) => {
if (!url || !url.startsWith("qortal://")) { if (!url || !url.startsWith('qortal://')) {
return null; return null;
} }
// Skip links starting with "qortal://use-" // Skip links starting with "qortal://use-"
if (url.startsWith("qortal://use-")) { if (url.startsWith('qortal://use-')) {
return null; return null;
} }
url = url.replace(/^(qortal\:\/\/)/, ""); url = url.replace(/^(qortal\:\/\/)/, '');
if (url.includes("/")) { if (url.includes('/')) {
let parts = url.split("/"); let parts = url.split('/');
const service = parts[0].toUpperCase(); const service = parts[0].toUpperCase();
parts.shift(); parts.shift();
const name = parts[0]; const name = parts[0];
parts.shift(); parts.shift();
let identifier; let identifier;
const path = parts.join("/"); const path = parts.join('/');
return { service, name, identifier, path }; return { service, name, identifier, path };
} }
@ -64,8 +64,7 @@ function processText(input) {
} }
const linkify = (text) => { 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; let textFormatted = text;
const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g; const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g;
textFormatted = text.replace(urlPattern, (url) => { textFormatted = text.replace(urlPattern, (url) => {
@ -75,22 +74,66 @@ const linkify = (text) => {
return processText(textFormatted); return processText(textFormatted);
}; };
export const MessageDisplay = ({ htmlContent, isReply }) => { export const MessageDisplay = ({ htmlContent, isReply }) => {
const sanitizedContent = useMemo(() => {
const sanitizedContent = useMemo(()=> {
return DOMPurify.sanitize(linkify(htmlContent), { return DOMPurify.sanitize(linkify(htmlContent), {
ALLOWED_TAGS: [ ALLOWED_TAGS: [
'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img', 'a',
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 's', 'hr' '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: [ ALLOWED_ATTR: [
'href', 'target', 'rel', 'class', 'src', 'alt', 'title', 'href',
'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url' 'target',
'rel',
'class',
'src',
'alt',
'title',
'width',
'height',
'style',
'align',
'valign',
'colspan',
'rowspan',
'border',
'cellpadding',
'cellspacing',
'data-url',
], ],
}).replace(/<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, ''); }).replace(
}, [htmlContent]) /<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g,
''
);
}, [htmlContent]);
const handleClick = async (e) => { const handleClick = async (e) => {
e.preventDefault(); e.preventDefault();
@ -98,7 +141,7 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
const target = e.target; const target = e.target;
if (target.tagName === 'A') { if (target.tagName === 'A') {
const href = target.getAttribute('href'); const href = target.getAttribute('href');
if(window?.electronAPI){ if (window?.electronAPI) {
window.electronAPI.openExternal(href); window.electronAPI.openExternal(href);
} else { } else {
window.open(href, '_system'); window.open(href, '_system');
@ -106,22 +149,22 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
} else if (target.getAttribute('data-url')) { } else if (target.getAttribute('data-url')) {
const url = target.getAttribute('data-url'); const url = target.getAttribute('data-url');
let copyUrl = url let copyUrl = url;
try { try {
copyUrl = copyUrl.replace(/^(qortal:\/\/)/, '') copyUrl = copyUrl.replace(/^(qortal:\/\/)/, '');
if (copyUrl.startsWith('use-')) { if (copyUrl.startsWith('use-')) {
// Handle the new 'use' format // Handle the new 'use' format
const parts = copyUrl.split('/') const parts = copyUrl.split('/');
const type = parts[0].split('-')[1] // e.g., 'group' from 'use-group' const type = parts[0].split('-')[1]; // e.g., 'group' from 'use-group'
parts.shift() parts.shift();
const action = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., 'invite' from 'action-invite' const action = parts.length > 0 ? parts[0].split('-')[1] : null; // e.g., 'invite' from 'action-invite'
parts.shift() parts.shift();
const idPrefix = parts.length > 0 ? parts[0].split('-')[0] : null // e.g., 'groupid' from 'groupid-321' 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' const id = parts.length > 0 ? parts[0].split('-')[1] : null; // e.g., '321' from 'groupid-321'
if(action === 'join'){ if (action === 'join') {
executeEvent("globalActionJoinGroup", { groupId: id}); executeEvent('globalActionJoinGroup', { groupId: id });
return return;
} }
} }
} catch (error) { } catch (error) {
@ -130,8 +173,8 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
const res = extractComponents(url); const res = extractComponents(url);
if (res) { if (res) {
const { service, name, identifier, path } = res; const { service, name, identifier, path } = res;
executeEvent("addTab", { data: { service, name, identifier, path } }); executeEvent('addTab', { data: { service, name, identifier, path } });
executeEvent("open-apps-mode", { }); executeEvent('open-apps-mode', {});
} }
} }
}; };
@ -141,14 +184,12 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
let embedData = null; let embedData = null;
if (embedLink) { if (embedLink) {
embedData = embedLink[0] embedData = embedLink[0];
} }
return ( return (
<> <>
{embedLink && ( {embedLink && <Embed embedLink={embedData} />}
<Embed embedLink={embedData} />
)}
<div <div
className={`tiptap ${isReply ? 'isReply' : ''}`} className={`tiptap ${isReply ? 'isReply' : ''}`}
dangerouslySetInnerHTML={{ __html: sanitizedContent }} dangerouslySetInnerHTML={{ __html: sanitizedContent }}

View File

@ -1,56 +1,82 @@
import { Message } from "@chatscope/chat-ui-kit-react"; import React, {
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react"; useCallback,
import { useInView } from "react-intersection-observer"; useContext,
import { MessageDisplay } from "./MessageDisplay"; useEffect,
import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material"; useMemo,
import { formatTimestamp } from "../../utils/time"; useState,
import { getBaseApi } from "../../background"; } from 'react';
import { MyContext, getBaseApiReact } from "../../App"; import { useInView } from 'react-intersection-observer';
import { generateHTML } from "@tiptap/react"; import { MessageDisplay } from './MessageDisplay';
import Highlight from "@tiptap/extension-highlight"; import {
import Mention from "@tiptap/extension-mention"; Avatar,
import StarterKit from "@tiptap/starter-kit"; Box,
import Underline from "@tiptap/extension-underline"; Button,
import { executeEvent } from "../../utils/events"; ButtonBase,
import { WrapperUserAction } from "../WrapperUserAction"; List,
import ReplyIcon from "@mui/icons-material/Reply"; ListItem,
import { Spacer } from "../../common/Spacer"; ListItemText,
import { ReactionPicker } from "../ReactionPicker"; 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 KeyOffIcon from '@mui/icons-material/KeyOff';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import TextStyle from '@tiptap/extension-text-style'; import TextStyle from '@tiptap/extension-text-style';
import { addressInfoKeySelector } from "../../atoms/global"; import level0Img from '../../assets/badges/level-0.png';
import { useRecoilValue } from "recoil"; import level1Img from '../../assets/badges/level-1.png';
import level0Img from "../../assets/badges/level-0.png" import level2Img from '../../assets/badges/level-2.png';
import level1Img from "../../assets/badges/level-1.png" import level3Img from '../../assets/badges/level-3.png';
import level2Img from "../../assets/badges/level-2.png" import level4Img from '../../assets/badges/level-4.png';
import level3Img from "../../assets/badges/level-3.png" import level5Img from '../../assets/badges/level-5.png';
import level4Img from "../../assets/badges/level-4.png" import level6Img from '../../assets/badges/level-6.png';
import level5Img from "../../assets/badges/level-5.png" import level7Img from '../../assets/badges/level-7.png';
import level6Img from "../../assets/badges/level-6.png" import level8Img from '../../assets/badges/level-8.png';
import level7Img from "../../assets/badges/level-7.png" import level9Img from '../../assets/badges/level-9.png';
import level8Img from "../../assets/badges/level-8.png" import level10Img from '../../assets/badges/level-10.png';
import level9Img from "../../assets/badges/level-9.png"
import level10Img from "../../assets/badges/level-10.png"
const getBadgeImg = (level)=> { const getBadgeImg = (level) => {
switch(level?.toString()){ switch (level?.toString()) {
case '0':
case '0': return level0Img return level0Img;
case '1': return level1Img case '1':
case '2': return level2Img return level1Img;
case '3': return level3Img case '2':
case '4': return level4Img return level2Img;
case '5': return level5Img case '3':
case '6': return level6Img return level3Img;
case '7': return level7Img case '4':
case '8': return level8Img return level4Img;
case '9': return level9Img case '5':
case '10': return level10Img return level5Img;
default: return level0Img 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(({
export const MessageItem = React.memo(
({
message, message,
onSeen, onSeen,
isLast, isLast,
@ -66,69 +92,65 @@ export const MessageItem = React.memo(({
isUpdating, isUpdating,
lastSignature, lastSignature,
onEdit, onEdit,
isPrivate isPrivate,
}) => { }) => {
const { getIndividualUserInfo } = useContext(MyContext);
const {getIndividualUserInfo} = useContext(MyContext)
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [selectedReaction, setSelectedReaction] = useState(null); const [selectedReaction, setSelectedReaction] = useState(null);
const [userInfo, setUserInfo] = useState(null) const [userInfo, setUserInfo] = useState(null);
useEffect(()=> { useEffect(() => {
const getInfo = async ()=> { const getInfo = async () => {
if(!message?.sender) return if (!message?.sender) return;
try { try {
const res = await getIndividualUserInfo(message?.sender) const res = await getIndividualUserInfo(message?.sender);
if(!res) return null if (!res) return null;
setUserInfo(res) setUserInfo(res);
} catch (error) { } catch (error) {
// //
} }
} };
getInfo() getInfo();
}, [message?.sender, getIndividualUserInfo]) }, [message?.sender, getIndividualUserInfo]);
const htmlText = useMemo(()=> { const htmlText = useMemo(() => {
if (message?.messageText) {
if(message?.messageText){
return generateHTML(message?.messageText, [ return generateHTML(message?.messageText, [
StarterKit, StarterKit,
Underline, Underline,
Highlight, Highlight,
Mention, Mention,
TextStyle TextStyle,
]) ]);
} }
}, [message?.editTimestamp]);
}, [message?.editTimestamp]) const htmlReply = useMemo(() => {
if (reply?.messageText) {
const htmlReply = useMemo(()=> {
if(reply?.messageText){
return generateHTML(reply?.messageText, [ return generateHTML(reply?.messageText, [
StarterKit, StarterKit,
Underline, Underline,
Highlight, Highlight,
Mention, Mention,
TextStyle TextStyle,
]) ]);
} }
}, [reply?.editTimestamp]);
}, [reply?.editTimestamp]) const userAvatarUrl = useMemo(() => {
return message?.senderName
const userAvatarUrl = useMemo(()=> { ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
message?.senderName message?.senderName
}/qortal_avatar?async=true` : '' }/qortal_avatar?async=true`
}, []) : '';
}, []);
const onSeenFunc = useCallback(()=> { const onSeenFunc = useCallback(() => {
onSeen(message.id); onSeen(message.id);
}, [message?.id]) }, [message?.id]);
const theme = useTheme();
return ( return (
<> <>
@ -138,81 +160,83 @@ const onSeenFunc = useCallback(()=> {
</div> </div>
)} )}
<MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}> <MessageWragger
lastMessage={lastSignature === message?.signature}
isLast={isLast}
onSeen={onSeenFunc}
>
<div <div
style={{ style={{
padding: "10px", backgroundColor: theme.palette.background.default,
backgroundColor: "#232428", borderRadius: '7px',
borderRadius: "7px", display: 'flex',
width: "95%", gap: '7px',
display: "flex", opacity: isTemp || isUpdating ? 0.5 : 1,
gap: "7px", padding: '10px',
opacity: (isTemp || isUpdating) ? 0.5 : 1, width: '95%',
}} }}
id={message?.signature} id={message?.signature}
> >
{isShowingAsReply ? ( {isShowingAsReply ? (
<ReplyIcon <ReplyIcon
sx={{ sx={{
fontSize: "30px", fontSize: '30px',
}} }}
/> />
) : ( ) : (
<Box sx={{ <Box
sx={{
alignItems: 'center',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '20px', gap: '20px',
alignItems: 'center' }}
}}> >
<WrapperUserAction <WrapperUserAction
disabled={myAddress === message?.sender} disabled={myAddress === message?.sender}
address={message?.sender} address={message?.sender}
name={message?.senderName} name={message?.senderName}
> >
<Avatar <Avatar
sx={{ sx={{
backgroundColor: "#27282c", backgroundColor: theme.palette.background.default,
color: "white", color: theme.palette.text.primary,
height: '40px', height: '40px',
width: '40px' width: '40px',
}} }}
alt={message?.senderName} alt={message?.senderName}
src={userAvatarUrl} src={userAvatarUrl}
> >
{message?.senderName?.charAt(0)} {message?.senderName?.charAt(0)}
</Avatar> </Avatar>
</WrapperUserAction> </WrapperUserAction>
<Tooltip disableFocusListener title={`level ${userInfo ?? 0}`}> <Tooltip disableFocusListener title={`level ${userInfo ?? 0}`}>
<img
style={{
<img style={{
visibility: userInfo !== undefined ? 'visible' : 'hidden', visibility: userInfo !== undefined ? 'visible' : 'hidden',
width: '30px', width: '30px',
height: 'auto' height: 'auto',
}} src={getBadgeImg(userInfo)} /> }}
src={getBadgeImg(userInfo)}
/>
</Tooltip> </Tooltip>
</Box> </Box>
)} )}
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "7px", gap: '7px',
width: "100%", height: isShowingAsReply && '40px',
height: isShowingAsReply && "40px", width: '100%',
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
width: "100%", justifyContent: 'space-between',
justifyContent: "space-between", width: '100%',
}} }}
> >
<WrapperUserAction <WrapperUserAction
@ -223,20 +247,22 @@ const onSeenFunc = useCallback(()=> {
<Typography <Typography
sx={{ sx={{
fontWight: 600, fontWight: 600,
fontFamily: "Inter", fontFamily: 'Inter',
color: "cadetBlue", color: 'cadetBlue',
}} }}
> >
{message?.senderName || message?.sender} {message?.senderName || message?.sender}
</Typography> </Typography>
</WrapperUserAction> </WrapperUserAction>
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
gap: '10px', gap: '10px',
alignItems: 'center' alignItems: 'center',
}}> }}
{message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && ( >
{message?.sender === myAddress &&
(!message?.isNotEncrypted || isPrivate === false) && (
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
onEdit(message); onEdit(message);
@ -255,15 +281,21 @@ const onSeenFunc = useCallback(()=> {
</ButtonBase> </ButtonBase>
)} )}
{!isShowingAsReply && handleReaction && ( {!isShowingAsReply && handleReaction && (
<ReactionPicker onReaction={(val)=> { <ReactionPicker
onReaction={(val) => {
if(reactions && reactions[val] && reactions[val]?.find((item)=> item?.sender === myAddress)){ if (
handleReaction(val, message, false) reactions &&
reactions[val] &&
reactions[val]?.find(
(item) => item?.sender === myAddress
)
) {
handleReaction(val, message, false);
} else { } else {
handleReaction(val, message, true) handleReaction(val, message, true);
} }
}}
}} /> />
)} )}
</Box> </Box>
</Box> </Box>
@ -272,40 +304,46 @@ const onSeenFunc = useCallback(()=> {
<Spacer height="20px" /> <Spacer height="20px" />
<Box <Box
sx={{ sx={{
width: "100%", backgroundColor: theme.palette.background.default,
borderRadius: "5px", borderRadius: '5px',
backgroundColor: "var(--bg-primary)", cursor: 'pointer',
overflow: 'hidden',
display: 'flex', display: 'flex',
gap: '20px', gap: '20px',
maxHeight: '90px', maxHeight: '90px',
cursor: 'pointer' overflow: 'hidden',
width: '100%',
}} }}
onClick={()=> { onClick={() => {
scrollToItem(replyIndex) scrollToItem(replyIndex);
}} }}
> >
<Box sx={{ <Box
sx={{
background: theme.palette.background.default,
height: '100%', height: '100%',
width: '5px', width: '5px',
background: 'white' }}
}} />
<Box sx={{
padding: '5px'
}}>
<Typography sx={{
fontSize: '12px',
fontWeight: 600
}}>Replied to {reply?.senderName || reply?.senderAddress}</Typography>
{reply?.messageText && (
<MessageDisplay
htmlContent={htmlReply}
/> />
<Box
sx={{
padding: '5px',
}}
>
<Typography
sx={{
fontSize: '12px',
fontWeight: 600,
}}
>
Replied to {reply?.senderName || reply?.senderAddress}
</Typography>
{reply?.messageText && (
<MessageDisplay htmlContent={htmlReply} />
)} )}
{reply?.decryptedData?.type === "notification" ? ( {reply?.decryptedData?.type === 'notification' ? (
<MessageDisplay htmlContent={reply.decryptedData?.data?.message} /> <MessageDisplay
htmlContent={reply.decryptedData?.data?.message}
/>
) : ( ) : (
<MessageDisplay isReply htmlContent={reply.text} /> <MessageDisplay isReply htmlContent={reply.text} />
)} )}
@ -313,56 +351,72 @@ const onSeenFunc = useCallback(()=> {
</Box> </Box>
</> </>
)} )}
{htmlText && ( {htmlText && <MessageDisplay htmlContent={htmlText} />}
{message?.decryptedData?.type === 'notification' ? (
<MessageDisplay <MessageDisplay
htmlContent={htmlText} htmlContent={message.decryptedData?.data?.message}
/> />
)}
{message?.decryptedData?.type === "notification" ? (
<MessageDisplay htmlContent={message.decryptedData?.data?.message} />
) : ( ) : (
<MessageDisplay htmlContent={message.text} /> <MessageDisplay htmlContent={message.text} />
)} )}
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
width: "100%", width: '100%',
}} }}
> >
<Box sx={{ <Box
display: 'flex', sx={{
alignItems: 'center', alignItems: 'center',
gap: '5px' display: 'flex',
}}> gap: '5px',
{reactions && Object.keys(reactions).map((reaction)=> { }}
const numberOfReactions = reactions[reaction]?.length >
{reactions &&
Object.keys(reactions).map((reaction) => {
const numberOfReactions = reactions[reaction]?.length;
// const myReaction = reactions // const myReaction = reactions
if(numberOfReactions === 0) return null if (numberOfReactions === 0) return null;
return ( return (
<ButtonBase key={reaction} sx={{ <ButtonBase
key={reaction}
sx={{
background: theme.palette.background.paper,
borderRadius: '7px',
height: '30px', height: '30px',
minWidth: '45px', minWidth: '45px',
background: 'var(--bg-2)', }}
borderRadius: '7px' onClick={(event) => {
}} onClick={(event) => {
event.stopPropagation(); // Prevent event bubbling event.stopPropagation(); // Prevent event bubbling
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
setSelectedReaction(reaction); setSelectedReaction(reaction);
}}> }}
<div style={{ >
fontSize: '16px' <div
}}>{reaction}</div> {numberOfReactions > 1 && ( style={{
<Typography sx={{ fontSize: '16px',
marginLeft: '4px' }}
}}>{' '} {numberOfReactions}</Typography> >
{reaction}
</div>{' '}
{numberOfReactions > 1 && (
<Typography
sx={{
marginLeft: '4px',
}}
>
{' '}
{numberOfReactions}
</Typography>
)} )}
</ButtonBase> </ButtonBase>
) );
})} })}
</Box> </Box>
{selectedReaction && ( {selectedReaction && (
<Popover <Popover
open={Boolean(anchorEl)} open={Boolean(anchorEl)}
@ -372,17 +426,18 @@ const onSeenFunc = useCallback(()=> {
setSelectedReaction(null); setSelectedReaction(null);
}} }}
anchorOrigin={{ anchorOrigin={{
vertical: "top", vertical: 'top',
horizontal: "center", horizontal: 'center',
}} }}
transformOrigin={{ transformOrigin={{
vertical: "bottom", vertical: 'bottom',
horizontal: "center", horizontal: 'center',
}} }}
PaperProps={{ PaperProps={{
// TODO: deprecated
style: { style: {
backgroundColor: "#232428", backgroundColor: theme.palette.background.default,
color: "white", color: theme.palette.text.primary,
}, },
}} }}
> >
@ -390,15 +445,20 @@ const onSeenFunc = useCallback(()=> {
<Typography variant="subtitle1" sx={{ marginBottom: 1 }}> <Typography variant="subtitle1" sx={{ marginBottom: 1 }}>
People who reacted with {selectedReaction} People who reacted with {selectedReaction}
</Typography> </Typography>
<List sx={{
<List
sx={{
overflow: 'auto', overflow: 'auto',
maxWidth: '300px', maxWidth: '300px',
maxHeight: '300px' maxHeight: '300px',
}}> }}
>
{reactions[selectedReaction]?.map((reactionItem) => ( {reactions[selectedReaction]?.map((reactionItem) => (
<ListItem key={reactionItem.sender}> <ListItem key={reactionItem.sender}>
<ListItemText <ListItemText
primary={reactionItem.senderName || reactionItem.sender} primary={
reactionItem.senderName || reactionItem.sender
}
/> />
</ListItem> </ListItem>
))} ))}
@ -424,53 +484,61 @@ const onSeenFunc = useCallback(()=> {
{reactions[selectedReaction]?.find( {reactions[selectedReaction]?.find(
(item) => item?.sender === myAddress (item) => item?.sender === myAddress
) )
? "Remove Reaction" ? 'Remove Reaction'
: "Add Reaction"} : 'Add Reaction'}
</Button> </Button>
</Box> </Box>
</Popover> </Popover>
)} )}
<Box sx={{ <Box
display: 'flex', sx={{
alignItems: 'center', alignItems: 'center',
gap: '15px' display: 'flex',
}}> gap: '15px',
}}
>
{message?.isNotEncrypted && isPrivate && ( {message?.isNotEncrypted && isPrivate && (
<KeyOffIcon sx={{ <KeyOffIcon
color: 'white', sx={{
marginLeft: '10px' color: theme.palette.text.primary,
}} /> marginLeft: '10px',
}}
/>
)} )}
{isUpdating ? ( {isUpdating ? (
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
color: "gray", color: theme.palette.text.secondary,
fontFamily: "Inter", fontFamily: 'Inter',
}} }}
> >
{message?.status === 'failed-permanent' ? 'Failed to update' : 'Updating...'} {message?.status === 'failed-permanent'
? 'Failed to update'
: 'Updating...'}
</Typography> </Typography>
) : isTemp ? ( ) : isTemp ? (
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
color: "gray", color: theme.palette.text.secondary,
fontFamily: "Inter", fontFamily: 'Inter',
}} }}
> >
{message?.status === 'failed-permanent' ? 'Failed to send' : 'Sending...'} {message?.status === 'failed-permanent'
? 'Failed to send'
: 'Sending...'}
</Typography> </Typography>
) : ( ) : (
<> <>
{message?.isEdit && ( {message?.isEdit && (
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
color: "gray", color: theme.palette.text.secondary,
fontFamily: "Inter", fontFamily: 'Inter',
fontStyle: 'italic' fontStyle: 'italic',
}} }}
> >
Edited Edited
@ -478,9 +546,9 @@ const onSeenFunc = useCallback(()=> {
)} )}
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
color: "gray", color: theme.palette.text.secondary,
fontFamily: "Inter", fontFamily: 'Inter',
}} }}
> >
{formatTimestamp(message.timestamp)} {formatTimestamp(message.timestamp)}
@ -490,49 +558,60 @@ const onSeenFunc = useCallback(()=> {
</Box> </Box>
</Box> </Box>
</Box> </Box>
</div> </div>
</MessageWragger> </MessageWragger>
</> </>
); );
}); }
);
export const ReplyPreview = ({ message, isEdit }) => {
export const ReplyPreview = ({message, isEdit})=> { const theme = useTheme();
return ( return (
<Box <Box
sx={{ sx={{
marginTop: '20px', backgroundColor: theme.palette.background.default,
width: "100%", borderRadius: '5px',
borderRadius: "5px", cursor: 'pointer',
backgroundColor: "var(--bg-primary)",
overflow: 'hidden',
display: 'flex', display: 'flex',
gap: '20px', gap: '20px',
marginTop: '20px',
maxHeight: '90px', maxHeight: '90px',
cursor: 'pointer' overflow: 'hidden',
width: '100%',
}} }}
> >
<Box sx={{ <Box
sx={{
background: theme.palette.background.default,
height: '100%', height: '100%',
width: '5px', width: '5px',
background: 'white' }}
}} /> />
<Box sx={{ <Box
padding: '5px' sx={{
}}> padding: '5px',
}}
>
{isEdit ? ( {isEdit ? (
<Typography sx={{ <Typography
sx={{
fontSize: '12px', fontSize: '12px',
fontWeight: 600 fontWeight: 600,
}}>Editing Message</Typography> }}
>
Editing Message
</Typography>
) : ( ) : (
<Typography sx={{ <Typography
sx={{
fontSize: '12px', fontSize: '12px',
fontWeight: 600 fontWeight: 600,
}}>Replied to {message?.senderName || message?.senderAddress}</Typography> }}
>
Replied to {message?.senderName || message?.senderAddress}
</Typography>
)} )}
{message?.messageText && ( {message?.messageText && (
@ -542,32 +621,33 @@ export const ReplyPreview = ({message, isEdit})=> {
Underline, Underline,
Highlight, Highlight,
Mention, Mention,
TextStyle TextStyle,
])} ])}
/> />
)} )}
{message?.decryptedData?.type === "notification" ? (
{message?.decryptedData?.type === 'notification' ? (
<MessageDisplay htmlContent={message.decryptedData?.data?.message} /> <MessageDisplay htmlContent={message.decryptedData?.data?.message} />
) : ( ) : (
<MessageDisplay isReply htmlContent={message.text} /> <MessageDisplay isReply htmlContent={message.text} />
)} )}
</Box> </Box>
</Box> </Box>
);
};
) const MessageWragger = ({ lastMessage, onSeen, isLast, children }) => {
} if (lastMessage) {
const MessageWragger = ({lastMessage, onSeen, isLast, children})=> {
if(lastMessage){
return ( return (
<WatchComponent onSeen={onSeen} isLast={isLast}>{children}</WatchComponent> <WatchComponent onSeen={onSeen} isLast={isLast}>
) {children}
</WatchComponent>
);
} }
return children return children;
} };
const WatchComponent = ({onSeen, isLast, children})=> { const WatchComponent = ({ onSeen, isLast, children }) => {
const { ref, inView } = useInView({ const { ref, inView } = useInView({
threshold: 0.7, // Fully visible threshold: 0.7, // Fully visible
triggerOnce: true, // Only trigger once when it becomes visible triggerOnce: true, // Only trigger once when it becomes visible
@ -577,20 +657,22 @@ const WatchComponent = ({onSeen, isLast, children})=> {
useEffect(() => { useEffect(() => {
if (inView && isLast && onSeen) { if (inView && isLast && onSeen) {
setTimeout(() => { setTimeout(() => {
onSeen(); onSeen();
}, 100); }, 100);
} }
}, [inView, isLast, onSeen]); }, [inView, isLast, onSeen]);
return <div ref={ref} style={{ return (
width: '100%', <div
ref={ref}
style={{
display: 'flex', display: 'flex',
justifyContent: 'center' justifyContent: 'center',
}}> width: '100%',
}}
>
{children} {children}
</div> </div>
);
} };

View File

@ -1,6 +1,6 @@
.tiptap { .tiptap {
margin-top: 0; margin-top: 0;
color: white; /* Set default font color to white */ color: ''; /* Set default font color to white */
width: 100%; width: 100%;
} }
@ -105,21 +105,21 @@
color: white; /* Ensure paragraph text color is white */ color: white; /* Ensure paragraph text color is white */
margin: 0px; margin: 0px;
} }
.tiptap p.is-editor-empty:first-child::before { .tiptap p.is-editor-empty:first-child::before {
color: #adb5bd; color: #adb5bd;
content: attr(data-placeholder); content: attr(data-placeholder);
float: left; float: left;
height: 0; height: 0;
pointer-events: none; pointer-events: none;
} }
.tiptap p:empty::before { .tiptap p:empty::before {
content: ''; content: '';
display: inline-block; display: inline-block;
} }
.tiptap a { .tiptap a {
color: cadetblue color: cadetblue;
} }
.tiptap img { .tiptap img {
@ -131,13 +131,12 @@
font-size: 12px !important; font-size: 12px !important;
} }
.tiptap [data-type="mention"] { .tiptap [data-type='mention'] {
box-decoration-break: clone; box-decoration-break: clone;
color: lightblue; color: lightblue;
padding: 0.1rem 0.3rem; padding: 0.1rem 0.3rem;
} }
.unread-divider { .unread-divider {
width: 90%; width: 90%;
color: white; color: white;