mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-05-07 10:17:51 +00:00
Rename style file and add theme to chat
This commit is contained in:
parent
2013561db7
commit
69ff35b776
@ -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(/<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, '');
|
||||
}, [htmlContent])
|
||||
}).replace(
|
||||
/<span[^>]*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,22 +149,22 @@ 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:\/\/)/, '')
|
||||
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
|
||||
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) {
|
||||
@ -130,8 +173,8 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
|
||||
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,14 +184,12 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
|
||||
let embedData = null;
|
||||
|
||||
if (embedLink) {
|
||||
embedData = embedLink[0]
|
||||
embedData = embedLink[0];
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{embedLink && (
|
||||
<Embed embedLink={embedData} />
|
||||
)}
|
||||
{embedLink && <Embed embedLink={embedData} />}
|
||||
<div
|
||||
className={`tiptap ${isReply ? 'isReply' : ''}`}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
|
||||
|
@ -1,56 +1,82 @@
|
||||
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(({
|
||||
};
|
||||
|
||||
export const MessageItem = React.memo(
|
||||
({
|
||||
message,
|
||||
onSeen,
|
||||
isLast,
|
||||
@ -66,69 +92,65 @@ export const MessageItem = React.memo(({
|
||||
isUpdating,
|
||||
lastSignature,
|
||||
onEdit,
|
||||
isPrivate
|
||||
}) => {
|
||||
|
||||
const {getIndividualUserInfo} = useContext(MyContext)
|
||||
isPrivate,
|
||||
}) => {
|
||||
const { getIndividualUserInfo } = useContext(MyContext);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [selectedReaction, setSelectedReaction] = useState(null);
|
||||
const [userInfo, setUserInfo] = useState(null)
|
||||
const [userInfo, setUserInfo] = useState(null);
|
||||
|
||||
useEffect(()=> {
|
||||
const getInfo = async ()=> {
|
||||
if(!message?.sender) return
|
||||
useEffect(() => {
|
||||
const getInfo = async () => {
|
||||
if (!message?.sender) return;
|
||||
try {
|
||||
const res = await getIndividualUserInfo(message?.sender)
|
||||
if(!res) return null
|
||||
setUserInfo(res)
|
||||
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){
|
||||
const htmlText = useMemo(() => {
|
||||
if (message?.messageText) {
|
||||
return generateHTML(message?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention,
|
||||
TextStyle
|
||||
])
|
||||
TextStyle,
|
||||
]);
|
||||
}
|
||||
}, [message?.editTimestamp]);
|
||||
|
||||
}, [message?.editTimestamp])
|
||||
|
||||
|
||||
|
||||
const htmlReply = useMemo(()=> {
|
||||
|
||||
if(reply?.messageText){
|
||||
const htmlReply = useMemo(() => {
|
||||
if (reply?.messageText) {
|
||||
return generateHTML(reply?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention,
|
||||
TextStyle
|
||||
])
|
||||
TextStyle,
|
||||
]);
|
||||
}
|
||||
}, [reply?.editTimestamp]);
|
||||
|
||||
}, [reply?.editTimestamp])
|
||||
|
||||
const userAvatarUrl = useMemo(()=> {
|
||||
return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
const userAvatarUrl = useMemo(() => {
|
||||
return message?.senderName
|
||||
? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
message?.senderName
|
||||
}/qortal_avatar?async=true` : ''
|
||||
}, [])
|
||||
}/qortal_avatar?async=true`
|
||||
: '';
|
||||
}, []);
|
||||
|
||||
const onSeenFunc = useCallback(()=> {
|
||||
const onSeenFunc = useCallback(() => {
|
||||
onSeen(message.id);
|
||||
}, [message?.id])
|
||||
}, [message?.id]);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -138,81 +160,83 @@ const onSeenFunc = useCallback(()=> {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}>
|
||||
|
||||
|
||||
<MessageWragger
|
||||
lastMessage={lastSignature === message?.signature}
|
||||
isLast={isLast}
|
||||
onSeen={onSeenFunc}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "10px",
|
||||
backgroundColor: "#232428",
|
||||
borderRadius: "7px",
|
||||
width: "95%",
|
||||
display: "flex",
|
||||
gap: "7px",
|
||||
opacity: (isTemp || isUpdating) ? 0.5 : 1,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: '7px',
|
||||
display: 'flex',
|
||||
gap: '7px',
|
||||
opacity: isTemp || isUpdating ? 0.5 : 1,
|
||||
padding: '10px',
|
||||
width: '95%',
|
||||
}}
|
||||
id={message?.signature}
|
||||
>
|
||||
{isShowingAsReply ? (
|
||||
<ReplyIcon
|
||||
sx={{
|
||||
fontSize: "30px",
|
||||
fontSize: '30px',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '20px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<WrapperUserAction
|
||||
disabled={myAddress === message?.sender}
|
||||
address={message?.sender}
|
||||
name={message?.senderName}
|
||||
>
|
||||
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
height: '40px',
|
||||
width: '40px'
|
||||
width: '40px',
|
||||
}}
|
||||
alt={message?.senderName}
|
||||
src={userAvatarUrl}
|
||||
>
|
||||
{message?.senderName?.charAt(0)}
|
||||
</Avatar>
|
||||
|
||||
|
||||
</WrapperUserAction>
|
||||
<Tooltip disableFocusListener title={`level ${userInfo ?? 0}`}>
|
||||
|
||||
|
||||
<img style={{
|
||||
<img
|
||||
style={{
|
||||
visibility: userInfo !== undefined ? 'visible' : 'hidden',
|
||||
width: '30px',
|
||||
height: 'auto'
|
||||
}} src={getBadgeImg(userInfo)} />
|
||||
height: 'auto',
|
||||
}}
|
||||
src={getBadgeImg(userInfo)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "7px",
|
||||
width: "100%",
|
||||
height: isShowingAsReply && "40px",
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '7px',
|
||||
height: isShowingAsReply && '40px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<WrapperUserAction
|
||||
@ -223,20 +247,22 @@ const onSeenFunc = useCallback(()=> {
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
fontFamily: 'Inter',
|
||||
color: 'cadetBlue',
|
||||
}}
|
||||
>
|
||||
{message?.senderName || message?.sender}
|
||||
</Typography>
|
||||
|
||||
</WrapperUserAction>
|
||||
<Box sx={{
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
{message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && (
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{message?.sender === myAddress &&
|
||||
(!message?.isNotEncrypted || isPrivate === false) && (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
onEdit(message);
|
||||
@ -255,15 +281,21 @@ const onSeenFunc = useCallback(()=> {
|
||||
</ButtonBase>
|
||||
)}
|
||||
{!isShowingAsReply && handleReaction && (
|
||||
<ReactionPicker onReaction={(val)=> {
|
||||
|
||||
if(reactions && reactions[val] && reactions[val]?.find((item)=> item?.sender === myAddress)){
|
||||
handleReaction(val, message, false)
|
||||
<ReactionPicker
|
||||
onReaction={(val) => {
|
||||
if (
|
||||
reactions &&
|
||||
reactions[val] &&
|
||||
reactions[val]?.find(
|
||||
(item) => item?.sender === myAddress
|
||||
)
|
||||
) {
|
||||
handleReaction(val, message, false);
|
||||
} else {
|
||||
handleReaction(val, message, true)
|
||||
handleReaction(val, message, true);
|
||||
}
|
||||
|
||||
}} />
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
@ -272,40 +304,46 @@ const onSeenFunc = useCallback(()=> {
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "var(--bg-primary)",
|
||||
overflow: 'hidden',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
maxHeight: '90px',
|
||||
cursor: 'pointer'
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
onClick={()=> {
|
||||
scrollToItem(replyIndex)
|
||||
|
||||
|
||||
onClick={() => {
|
||||
scrollToItem(replyIndex);
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
<Box
|
||||
sx={{
|
||||
background: theme.palette.background.default,
|
||||
height: '100%',
|
||||
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" ? (
|
||||
<MessageDisplay htmlContent={reply.decryptedData?.data?.message} />
|
||||
{reply?.decryptedData?.type === 'notification' ? (
|
||||
<MessageDisplay
|
||||
htmlContent={reply.decryptedData?.data?.message}
|
||||
/>
|
||||
) : (
|
||||
<MessageDisplay isReply htmlContent={reply.text} />
|
||||
)}
|
||||
@ -313,56 +351,72 @@ const onSeenFunc = useCallback(()=> {
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{htmlText && (
|
||||
{htmlText && <MessageDisplay htmlContent={htmlText} />}
|
||||
|
||||
{message?.decryptedData?.type === 'notification' ? (
|
||||
<MessageDisplay
|
||||
htmlContent={htmlText}
|
||||
htmlContent={message.decryptedData?.data?.message}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{message?.decryptedData?.type === "notification" ? (
|
||||
<MessageDisplay htmlContent={message.decryptedData?.data?.message} />
|
||||
) : (
|
||||
<MessageDisplay htmlContent={message.text} />
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
gap: '5px'
|
||||
}}>
|
||||
{reactions && Object.keys(reactions).map((reaction)=> {
|
||||
const numberOfReactions = reactions[reaction]?.length
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
}}
|
||||
>
|
||||
{reactions &&
|
||||
Object.keys(reactions).map((reaction) => {
|
||||
const numberOfReactions = reactions[reaction]?.length;
|
||||
// const myReaction = reactions
|
||||
if(numberOfReactions === 0) return null
|
||||
if (numberOfReactions === 0) return null;
|
||||
return (
|
||||
<ButtonBase key={reaction} sx={{
|
||||
<ButtonBase
|
||||
key={reaction}
|
||||
sx={{
|
||||
background: theme.palette.background.paper,
|
||||
borderRadius: '7px',
|
||||
height: '30px',
|
||||
minWidth: '45px',
|
||||
background: 'var(--bg-2)',
|
||||
borderRadius: '7px'
|
||||
}} onClick={(event) => {
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation(); // Prevent event bubbling
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedReaction(reaction);
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '16px'
|
||||
}}>{reaction}</div> {numberOfReactions > 1 && (
|
||||
<Typography sx={{
|
||||
marginLeft: '4px'
|
||||
}}>{' '} {numberOfReactions}</Typography>
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
{reaction}
|
||||
</div>{' '}
|
||||
{numberOfReactions > 1 && (
|
||||
<Typography
|
||||
sx={{
|
||||
marginLeft: '4px',
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{numberOfReactions}
|
||||
</Typography>
|
||||
)}
|
||||
</ButtonBase>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{selectedReaction && (
|
||||
<Popover
|
||||
open={Boolean(anchorEl)}
|
||||
@ -372,17 +426,18 @@ const onSeenFunc = useCallback(()=> {
|
||||
setSelectedReaction(null);
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
PaperProps={{
|
||||
// TODO: deprecated
|
||||
style: {
|
||||
backgroundColor: "#232428",
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@ -390,15 +445,20 @@ const onSeenFunc = useCallback(()=> {
|
||||
<Typography variant="subtitle1" sx={{ marginBottom: 1 }}>
|
||||
People who reacted with {selectedReaction}
|
||||
</Typography>
|
||||
<List sx={{
|
||||
|
||||
<List
|
||||
sx={{
|
||||
overflow: 'auto',
|
||||
maxWidth: '300px',
|
||||
maxHeight: '300px'
|
||||
}}>
|
||||
maxHeight: '300px',
|
||||
}}
|
||||
>
|
||||
{reactions[selectedReaction]?.map((reactionItem) => (
|
||||
<ListItem key={reactionItem.sender}>
|
||||
<ListItemText
|
||||
primary={reactionItem.senderName || reactionItem.sender}
|
||||
primary={
|
||||
reactionItem.senderName || reactionItem.sender
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
@ -424,53 +484,61 @@ const onSeenFunc = useCallback(()=> {
|
||||
{reactions[selectedReaction]?.find(
|
||||
(item) => item?.sender === myAddress
|
||||
)
|
||||
? "Remove Reaction"
|
||||
: "Add Reaction"}
|
||||
? 'Remove Reaction'
|
||||
: 'Add Reaction'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Popover>
|
||||
)}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
gap: '15px'
|
||||
}}>
|
||||
display: 'flex',
|
||||
gap: '15px',
|
||||
}}
|
||||
>
|
||||
{message?.isNotEncrypted && isPrivate && (
|
||||
<KeyOffIcon sx={{
|
||||
color: 'white',
|
||||
marginLeft: '10px'
|
||||
}} />
|
||||
<KeyOffIcon
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
marginLeft: '10px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isUpdating ? (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: "gray",
|
||||
fontFamily: "Inter",
|
||||
fontSize: '14px',
|
||||
color: theme.palette.text.secondary,
|
||||
fontFamily: 'Inter',
|
||||
}}
|
||||
>
|
||||
{message?.status === 'failed-permanent' ? 'Failed to update' : 'Updating...'}
|
||||
{message?.status === 'failed-permanent'
|
||||
? 'Failed to update'
|
||||
: 'Updating...'}
|
||||
</Typography>
|
||||
) : isTemp ? (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: "gray",
|
||||
fontFamily: "Inter",
|
||||
fontSize: '14px',
|
||||
color: theme.palette.text.secondary,
|
||||
fontFamily: 'Inter',
|
||||
}}
|
||||
>
|
||||
{message?.status === 'failed-permanent' ? 'Failed to send' : 'Sending...'}
|
||||
{message?.status === 'failed-permanent'
|
||||
? 'Failed to send'
|
||||
: 'Sending...'}
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
{message?.isEdit && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: "gray",
|
||||
fontFamily: "Inter",
|
||||
fontStyle: 'italic'
|
||||
fontSize: '14px',
|
||||
color: theme.palette.text.secondary,
|
||||
fontFamily: 'Inter',
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
Edited
|
||||
@ -478,9 +546,9 @@ const onSeenFunc = useCallback(()=> {
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
color: "gray",
|
||||
fontFamily: "Inter",
|
||||
fontSize: '14px',
|
||||
color: theme.palette.text.secondary,
|
||||
fontFamily: 'Inter',
|
||||
}}
|
||||
>
|
||||
{formatTimestamp(message.timestamp)}
|
||||
@ -490,49 +558,60 @@ const onSeenFunc = useCallback(()=> {
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
</div>
|
||||
</MessageWragger>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
export const ReplyPreview = ({message, isEdit})=> {
|
||||
export const ReplyPreview = ({ message, isEdit }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: '20px',
|
||||
width: "100%",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "var(--bg-primary)",
|
||||
overflow: 'hidden',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
marginTop: '20px',
|
||||
maxHeight: '90px',
|
||||
cursor: 'pointer'
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
<Box
|
||||
sx={{
|
||||
background: theme.palette.background.default,
|
||||
height: '100%',
|
||||
width: '5px',
|
||||
background: 'white'
|
||||
}} />
|
||||
<Box sx={{
|
||||
padding: '5px'
|
||||
}}>
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
padding: '5px',
|
||||
}}
|
||||
>
|
||||
{isEdit ? (
|
||||
<Typography sx={{
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 600
|
||||
}}>Editing Message</Typography>
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Editing Message
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography sx={{
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 600
|
||||
}}>Replied to {message?.senderName || message?.senderAddress}</Typography>
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Replied to {message?.senderName || message?.senderAddress}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{message?.messageText && (
|
||||
@ -542,32 +621,33 @@ export const ReplyPreview = ({message, isEdit})=> {
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention,
|
||||
TextStyle
|
||||
TextStyle,
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
{message?.decryptedData?.type === "notification" ? (
|
||||
|
||||
{message?.decryptedData?.type === 'notification' ? (
|
||||
<MessageDisplay htmlContent={message.decryptedData?.data?.message} />
|
||||
) : (
|
||||
<MessageDisplay isReply htmlContent={message.text} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const MessageWragger = ({lastMessage, onSeen, isLast, children})=> {
|
||||
|
||||
if(lastMessage){
|
||||
const MessageWragger = ({ lastMessage, onSeen, isLast, children }) => {
|
||||
if (lastMessage) {
|
||||
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({
|
||||
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();
|
||||
}, 100);
|
||||
|
||||
}
|
||||
}, [inView, isLast, onSeen]);
|
||||
|
||||
return <div ref={ref} style={{
|
||||
width: '100%',
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -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,21 +105,21 @@
|
||||
color: white; /* Ensure paragraph text color is white */
|
||||
margin: 0px;
|
||||
}
|
||||
.tiptap p.is-editor-empty:first-child::before {
|
||||
.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 {
|
||||
.tiptap p:empty::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.tiptap a {
|
||||
color: cadetblue
|
||||
color: cadetblue;
|
||||
}
|
||||
|
||||
.tiptap img {
|
||||
@ -131,13 +131,12 @@
|
||||
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;
|
Loading…
x
Reference in New Issue
Block a user