Add theme to all chat pages

This commit is contained in:
Nicola Benaglia 2025-04-19 16:00:58 +02:00
parent cf335a6d0a
commit 905cddf29a
9 changed files with 620 additions and 541 deletions

View File

@ -4,7 +4,7 @@ import {
AuthenticatedContainerInnerTop, AuthenticatedContainerInnerTop,
CustomButton, CustomButton,
} from '../../styles/App-styles'; } from '../../styles/App-styles';
import { Box, CircularProgress } from '@mui/material'; import { Box, CircularProgress, useTheme } from '@mui/material';
import { objectToBase64 } from '../../qdn/encryption/group-encryption'; import { objectToBase64 } from '../../qdn/encryption/group-encryption';
import ShortUniqueId from 'short-unique-id'; import ShortUniqueId from 'short-unique-id';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
@ -39,6 +39,7 @@ export const AnnouncementDiscussion = ({
myName, myName,
isPrivate, isPrivate,
}) => { }) => {
const theme = useTheme();
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isFocusedParent, setIsFocusedParent] = useState(false); const [isFocusedParent, setIsFocusedParent] = useState(false);
@ -212,6 +213,7 @@ export const AnnouncementDiscussion = ({
getData({ name: data.name, identifier: data.identifier }, isPrivate); getData({ name: data.name, identifier: data.identifier }, isPrivate);
} }
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@ -274,19 +276,19 @@ export const AnnouncementDiscussion = ({
return ( return (
<div <div
style={{ style={{
height: isMobile ? '100%' : '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: isMobile ? '100%' : '100%',
width: '100%', width: '100%',
}} }}
> >
<div <div
style={{ style={{
position: 'relative',
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexShrink: 0, flexShrink: 0,
position: 'relative',
width: '100%',
}} }}
> >
<AuthenticatedContainerInnerTop <AuthenticatedContainerInnerTop
@ -301,6 +303,7 @@ export const AnnouncementDiscussion = ({
}} }}
/> />
</AuthenticatedContainerInnerTop> </AuthenticatedContainerInnerTop>
<Spacer height="20px" /> <Spacer height="20px" />
</div> </div>
<AnnouncementList <AnnouncementList
@ -314,30 +317,27 @@ export const AnnouncementDiscussion = ({
/> />
<div <div
style={{ style={{
// position: 'fixed', backgroundColor: theme.palette.background.default,
// bottom: '0px', bottom: isFocusedParent ? '0px' : 'unset',
backgroundColor: '#232428', boxSizing: 'border-box',
minHeight: isMobile ? '0px' : '150px',
maxHeight: isMobile ? 'auto' : '400px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
padding: isMobile ? '10px' : '20px',
position: isFocusedParent ? 'fixed' : 'relative',
bottom: isFocusedParent ? '0px' : 'unset',
top: isFocusedParent ? '0px' : 'unset',
zIndex: isFocusedParent ? 5 : 'unset',
flexShrink: 0, flexShrink: 0,
maxHeight: '400px',
minHeight: '150px',
overflow: 'hidden',
padding: '20px',
position: isFocusedParent ? 'fixed' : 'relative',
top: isFocusedParent ? '0px' : 'unset',
width: '100%',
zIndex: isFocusedParent ? 5 : 'unset',
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
// height: '100%', flexGrow: 1,
flexGrow: isMobile && 1,
overflow: 'auto', overflow: 'auto',
}} }}
> >
@ -353,11 +353,11 @@ export const AnnouncementDiscussion = ({
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100&', flexShrink: 0,
gap: '10px', gap: '10px',
justifyContent: 'center', justifyContent: 'center',
flexShrink: 0,
position: 'relative', position: 'relative',
width: '100&',
}} }}
> >
{isFocusedParent && ( {isFocusedParent && (
@ -369,13 +369,13 @@ export const AnnouncementDiscussion = ({
// Unfocus the editor // Unfocus the editor
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
background: 'red',
cursor: isSending ? 'default' : 'pointer', cursor: isSending ? 'default' : 'pointer',
flexShrink: 0, flexShrink: 0,
padding: isMobile && '5px', fontSize: '14px',
fontSize: isMobile && '14px', marginTop: 'auto',
background: 'red', padding: '5px',
}} }}
> >
{` Close`} {` Close`}
@ -387,25 +387,25 @@ export const AnnouncementDiscussion = ({
publishComment(); publishComment();
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
background: theme.palette.background.default,
cursor: isSending ? 'default' : 'pointer', cursor: isSending ? 'default' : 'pointer',
background: isSending && 'rgba(0, 0, 0, 0.8)',
flexShrink: 0, flexShrink: 0,
padding: isMobile && '5px', fontSize: '14px',
fontSize: isMobile && '14px', marginTop: 'auto',
padding: '5px',
}} }}
> >
{isSending && ( {isSending && (
<CircularProgress <CircularProgress
size={18} size={18}
sx={{ sx={{
color: theme.palette.text.primary,
left: '50%',
marginLeft: '-12px',
marginTop: '-12px',
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
color: 'white',
}} }}
/> />
)} )}

View File

@ -1,173 +1,206 @@
import { Message } from "@chatscope/chat-ui-kit-react"; import React, { useEffect, useState } from 'react';
import React, { useEffect, useState } from "react"; import { MessageDisplay } from './MessageDisplay';
import { useInView } from "react-intersection-observer"; import { Avatar, Box, Typography, useTheme } from '@mui/material';
import { MessageDisplay } from "./MessageDisplay"; import { formatTimestamp } from '../../utils/time';
import { Avatar, Box, Typography } from "@mui/material";
import { formatTimestamp } from "../../utils/time";
import ChatBubbleIcon from '@mui/icons-material/ChatBubble'; import ChatBubbleIcon from '@mui/icons-material/ChatBubble';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import { getBaseApi } from "../../background"; import { getBaseApi } from '../../background';
import { requestQueueCommentCount } from "./GroupAnnouncements"; import { requestQueueCommentCount } from './GroupAnnouncements';
import { CustomLoader } from "../../common/CustomLoader"; import { CustomLoader } from '../../common/CustomLoader';
import { getArbitraryEndpointReact, getBaseApiReact } from "../../App"; import { getArbitraryEndpointReact, getBaseApiReact } from '../../App';
import { WrapperUserAction } from "../WrapperUserAction"; import { WrapperUserAction } from '../WrapperUserAction';
export const AnnouncementItem = ({ message, messageData, setSelectedAnnouncement, disableComment, myName }) => {
const [commentLength, setCommentLength] = useState(0) export const AnnouncementItem = ({
const getNumberOfComments = React.useCallback( message,
async () => { messageData,
try { setSelectedAnnouncement,
const offset = 0; disableComment,
myName,
}) => {
const theme = useTheme();
const [commentLength, setCommentLength] = useState(0);
const getNumberOfComments = React.useCallback(async () => {
try {
const offset = 0;
// dispatch(setIsLoadingGlobal(true)) // dispatch(setIsLoadingGlobal(true))
const identifier = `cm-${message.identifier}`; const identifier = `cm-${message.identifier}`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=0&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=0&includemetadata=false&offset=${offset}&reverse=true&prefix=true`;
const response = await requestQueueCommentCount.enqueue(() => { const response = await requestQueueCommentCount.enqueue(() => {
return fetch(url, { return fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
}) });
const responseData = await response.json(); const responseData = await response.json();
setCommentLength(responseData?.length);
} catch (error) {
console.log(error);
}
}, []);
useEffect(() => {
if (disableComment) return;
getNumberOfComments();
}, []);
setCommentLength(responseData?.length);
} catch (error) {
} finally {
// dispatch(setIsLoadingGlobal(false))
}
},
[]
);
useEffect(()=> {
if(disableComment) return
getNumberOfComments()
}, [])
return ( return (
<div <div
style={{ style={{
padding: "10px", backgroundColor: theme.palette.background.default,
backgroundColor: "#232428", borderRadius: '7px',
borderRadius: "7px", display: 'flex',
width: "95%", flexDirection: 'column',
display: "flex",
gap: '7px', gap: '7px',
flexDirection: 'column' padding: '10px',
width: '95%',
}} }}
> >
<Box sx={{
display: "flex",
gap: '7px',
width: '100%',
wordBreak: 'break-word'
}}>
<WrapperUserAction disabled={myName === message?.name} address={undefined} name={message?.name}>
<Avatar
sx={{
backgroundColor: '#27282c',
color: 'white'
}}
alt={message?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.name}/qortal_avatar?async=true`}
>
{message?.name?.charAt(0)}
</Avatar>
</WrapperUserAction>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", gap: '7px',
gap: "7px", width: '100%',
width: '100%' wordBreak: 'break-word',
}} }}
> >
<WrapperUserAction disabled={myName === message?.name} address={undefined} name={message?.name}> <WrapperUserAction
<Typography disabled={myName === message?.name}
address={undefined}
name={message?.name}
>
<Avatar
sx={{
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}}
alt={message?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.name}/qortal_avatar?async=true`}
>
{message?.name?.charAt(0)}
</Avatar>
</WrapperUserAction>
<Box
sx={{ sx={{
fontWight: 600, display: 'flex',
fontFamily: "Inter", flexDirection: 'column',
color: "cadetBlue", gap: '7px',
width: '100%',
}} }}
> >
{message?.name} <WrapperUserAction
</Typography> disabled={myName === message?.name}
</WrapperUserAction> address={undefined}
{!messageData?.decryptedData && ( name={message?.name}
<Box sx={{ >
width: '100%', <Typography
display: 'flex', sx={{
justifyContent: 'center' fontWight: 600,
}}> fontFamily: 'Inter',
<CustomLoader /> color: 'cadetBlue',
</Box> }}
)} >
{messageData?.decryptedData?.message && ( {message?.name}
<> </Typography>
{messageData?.type === "notification" ? ( </WrapperUserAction>
<MessageDisplay htmlContent={messageData?.decryptedData?.message} /> {!messageData?.decryptedData && (
) : ( <Box
<MessageDisplay htmlContent={messageData?.decryptedData?.message} /> sx={{
)} width: '100%',
</> display: 'flex',
)} justifyContent: 'center',
}}
>
<CustomLoader />
</Box>
)}
{messageData?.decryptedData?.message && (
<>
{messageData?.type === 'notification' ? (
<MessageDisplay
htmlContent={messageData?.decryptedData?.message}
/>
) : (
<MessageDisplay
htmlContent={messageData?.decryptedData?.message}
/>
)}
</>
)}
<Box
<Box sx={{ sx={{
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
width: '100%' width: '100%',
}}> }}
<Typography sx={{ >
fontSize: '14px', <Typography
color: 'gray', sx={{
fontFamily: 'Inter' color: theme.palette.text.secondary,
}}>{formatTimestamp(message.created)}</Typography> fontFamily: 'Inter',
fontSize: '14px',
}}
>
{formatTimestamp(message.created)}
</Typography>
</Box>
</Box> </Box>
</Box> </Box>
</Box> {!disableComment && (
{!disableComment && ( <Box
<Box sx={{ sx={{
display: 'flex', alignItems: 'center',
width: '100%', borderTop: '1px solid white',
alignItems: 'center', cursor: 'pointer',
justifyContent: 'space-between', display: 'flex',
padding: '20px', justifyContent: 'space-between',
cursor: 'pointer', opacity: 0.4,
opacity: 0.4, padding: '20px',
borderTop: '1px solid white', width: '100%',
}}
}} onClick={()=> setSelectedAnnouncement(message)}> onClick={() => setSelectedAnnouncement(message)}
>
<Box sx={{ <Box
display: 'flex', sx={{
width: '100%', alignItems: 'center',
gap: '25px', display: 'flex',
alignItems: 'center', gap: '25px',
width: '100%',
}}> }}
<ChatBubbleIcon sx={{ >
fontSize: '20px' <ChatBubbleIcon
}} /> sx={{
{commentLength ? ( fontSize: '20px',
<Typography sx={{ }}
fontSize: '14px' />
}}>{`${commentLength > 1 ? `${commentLength} comments` : `${commentLength} comment`}`}</Typography> {commentLength ? (
) : ( <Typography
<Typography sx={{ sx={{
fontSize: '14px' fontSize: '14px',
}}>Leave comment</Typography> }}
)} >{`${commentLength > 1 ? `${commentLength} comments` : `${commentLength} comment`}`}</Typography>
) : (
<Typography
sx={{
fontSize: '14px',
}}
>
Leave comment
</Typography>
)}
</Box>
<ArrowForwardIosIcon
sx={{
fontSize: '20px',
}}
/>
</Box> </Box>
<ArrowForwardIosIcon sx={{ )}
fontSize: '20px'
}} />
</Box>
)}
</div> </div>
); );
}; };

View File

@ -1,10 +1,5 @@
import React, { useCallback, useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { import { CellMeasurerCache } from 'react-virtualized';
List,
AutoSizer,
CellMeasurerCache,
CellMeasurer,
} from 'react-virtualized';
import { AnnouncementItem } from './AnnouncementItem'; import { AnnouncementItem } from './AnnouncementItem';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { CustomButton } from '../../styles/App-styles'; import { CustomButton } from '../../styles/App-styles';
@ -37,12 +32,12 @@ export const AnnouncementList = ({
return ( return (
<div <div
style={{ style={{
position: 'relative',
flexGrow: 1,
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1,
flexShrink: 1, flexShrink: 1,
position: 'relative',
width: '100%',
overflow: 'auto', overflow: 'auto',
}} }}
> >
@ -57,11 +52,11 @@ export const AnnouncementList = ({
<div <div
key={message?.identifier} key={message?.identifier}
style={{ style={{
marginBottom: '10px', alignItems: 'center',
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', marginBottom: '10px',
width: '100%',
}} }}
> >
<AnnouncementItem <AnnouncementItem
@ -89,10 +84,10 @@ export const AnnouncementList = ({
</AutoSizer> */} </AutoSizer> */}
<Box <Box
sx={{ sx={{
width: '100%',
marginTop: '25px',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
marginTop: '25px',
width: '100%',
}} }}
> >
{showLoadMore && ( {showLoadMore && (

View File

@ -7,15 +7,10 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { CreateCommonSecret } from './CreateCommonSecret';
import { reusableGet } from '../../qdn/publish/pubish';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
import { import {
base64ToUint8Array,
decodeBase64ForUIChatMessages, decodeBase64ForUIChatMessages,
objectToBase64, objectToBase64,
} from '../../qdn/encryption/group-encryption'; } from '../../qdn/encryption/group-encryption';
import { ChatContainerComp } from './ChatContainer';
import { ChatList } from './ChatList'; import { ChatList } from './ChatList';
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css'; import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
import Tiptap from './TipTap'; import Tiptap from './TipTap';
@ -38,7 +33,7 @@ import {
subscribeToEvent, subscribeToEvent,
unsubscribeFromEvent, unsubscribeFromEvent,
} from '../../utils/events'; } from '../../utils/events';
import { Box, ButtonBase, Divider, Typography } from '@mui/material'; import { Box, ButtonBase, Divider, Typography, useTheme } from '@mui/material';
import ShortUniqueId from 'short-unique-id'; import ShortUniqueId from 'short-unique-id';
import { ReplyPreview } from './MessageItem'; import { ReplyPreview } from './MessageItem';
import { ExitIcon } from '../../assets/Icons/ExitIcon'; import { ExitIcon } from '../../assets/Icons/ExitIcon';
@ -1001,16 +996,18 @@ export const ChatGroup = ({
setIsOpenQManager(true); setIsOpenQManager(true);
}, []); }, []);
const theme = useTheme();
return ( return (
<div <div
style={{ style={{
height: isMobile ? '100%' : '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
width: '100%', height: '100%',
left: hide && '-100000px',
opacity: hide ? 0 : 1, opacity: hide ? 0 : 1,
position: hide ? 'absolute' : 'relative', position: hide ? 'absolute' : 'relative',
left: hide && '-100000px', width: '100%',
}} }}
> >
<ChatList <ChatList
@ -1035,40 +1032,38 @@ export const ChatGroup = ({
{(!!secretKey || isPrivate === false) && ( {(!!secretKey || isPrivate === false) && (
<div <div
style={{ style={{
// position: 'fixed', backgroundColor: theme.palette.background.default,
// bottom: '0px', bottom: isFocusedParent ? '0px' : 'unset',
backgroundColor: '#232428', boxSizing: 'border-box',
minHeight: isMobile ? '0px' : '150px',
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
padding: isMobile ? '10px' : '20px',
position: isFocusedParent ? 'fixed' : 'relative',
bottom: isFocusedParent ? '0px' : 'unset',
top: isFocusedParent ? '0px' : 'unset',
zIndex: isFocusedParent ? 5 : 'unset',
flexShrink: 0, flexShrink: 0,
minHeight: '150px',
overflow: 'hidden',
padding: '20px',
position: isFocusedParent ? 'fixed' : 'relative',
top: isFocusedParent ? '0px' : 'unset',
width: '100%',
zIndex: isFocusedParent ? 5 : 'unset',
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: isMobile && 1, flexGrow: 1,
overflow: !isMobile && 'auto',
flexShrink: 0, flexShrink: 0,
width: 'calc(100% - 100px)',
justifyContent: 'flex-end', justifyContent: 'flex-end',
overflow: 'auto',
width: 'calc(100% - 100px)',
}} }}
> >
{replyMessage && ( {replyMessage && (
<Box <Box
sx={{ sx={{
alignItems: 'flex-start',
display: 'flex', display: 'flex',
gap: '5px', gap: '5px',
alignItems: 'flex-start',
width: '100%', width: '100%',
}} }}
> >
@ -1088,9 +1083,9 @@ export const ChatGroup = ({
{onEditMessage && ( {onEditMessage && (
<Box <Box
sx={{ sx={{
alignItems: 'flex-start',
display: 'flex', display: 'flex',
gap: '5px', gap: '5px',
alignItems: 'flex-start',
width: '100%', width: '100%',
}} }}
> >
@ -1123,9 +1118,9 @@ export const ChatGroup = ({
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100%',
justifyContent: 'flex-start', justifyContent: 'flex-start',
position: 'relative', position: 'relative',
width: '100%',
}} }}
> >
<Typography <Typography
@ -1141,11 +1136,11 @@ export const ChatGroup = ({
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100px', flexShrink: 0,
gap: '10px', gap: '10px',
justifyContent: 'center', justifyContent: 'center',
flexShrink: 0,
position: 'relative', position: 'relative',
width: '100px',
}} }}
> >
<CustomButton <CustomButton
@ -1154,26 +1149,28 @@ export const ChatGroup = ({
sendMessage(); sendMessage();
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
background: isSending
? theme.palette.background.default
: theme.palette.background.paper,
cursor: isSending ? 'default' : 'pointer', cursor: isSending ? 'default' : 'pointer',
background: isSending && 'rgba(0, 0, 0, 0.8)',
flexShrink: 0, flexShrink: 0,
marginTop: 'auto',
minWidth: 'auto',
padding: '5px', padding: '5px',
width: '100px', width: '100px',
minWidth: 'auto',
}} }}
> >
{isSending && ( {isSending && (
<CircularProgress <CircularProgress
size={18} size={18}
sx={{ sx={{
color: theme.palette.text.primary,
left: '50%',
marginLeft: '-12px',
marginTop: '-12px',
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
color: 'white',
}} }}
/> />
)} )}
@ -1185,25 +1182,24 @@ export const ChatGroup = ({
{isOpenQManager !== null && ( {isOpenQManager !== null && (
<Box <Box
sx={{ sx={{
position: 'fixed', backgroundColor: theme.palette.background.default,
height: '600px',
maxHeight: '100vh',
width: '400px',
maxWidth: '100vw',
backgroundColor: '#27282c',
zIndex: 100,
bottom: 0,
right: 0,
overflow: 'hidden',
borderTopLeftRadius: '10px', borderTopLeftRadius: '10px',
borderTopRightRadius: '10px', borderTopRightRadius: '10px',
bottom: 0,
boxShadow: 4,
display: hideView display: hideView
? 'none' ? 'none'
: isOpenQManager === true : isOpenQManager === true
? 'block' ? 'block'
: 'none', : 'none',
boxShadow: 4, height: '600px',
maxHeight: '100vh',
maxWidth: '100vw',
overflow: 'hidden',
position: 'fixed',
right: 0,
width: '400px',
zIndex: 100,
}} }}
> >
<Box <Box
@ -1214,12 +1210,11 @@ export const ChatGroup = ({
> >
<Box <Box
sx={{ sx={{
height: '40px',
display: 'flex',
alignItems: 'center', alignItems: 'center',
padding: '5px', display: 'flex',
height: '40px',
justifyContent: 'space-between', justifyContent: 'space-between',
padding: '5px',
}} }}
> >
<Typography>Q-Manager</Typography> <Typography>Q-Manager</Typography>
@ -1251,12 +1246,14 @@ export const ChatGroup = ({
)} )}
{/* <ChatContainerComp messages={formatMessages} /> */} {/* <ChatContainerComp messages={formatMessages} /> */}
<LoadingSnackbar <LoadingSnackbar
open={isLoading} open={isLoading}
info={{ info={{
message: 'Loading chat... please wait.', message: 'Loading chat... please wait.',
}} }}
/> />
<CustomizedSnackbars <CustomizedSnackbars
open={openSnack} open={openSnack}
setOpen={setOpenSnack} setOpen={setOpenSnack}

View File

@ -1,35 +1,56 @@
import { Box, Button, Typography } from '@mui/material' import React, { useContext } from 'react';
import React, { useContext } from 'react' import { Box, Button, Typography, useTheme } from '@mui/material';
import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { MyContext, getArbitraryEndpointReact, getBaseApiReact, pauseAllQueues } from '../../App'; import {
MyContext,
getArbitraryEndpointReact,
getBaseApiReact,
pauseAllQueues,
} from '../../App';
import { getFee } from '../../background'; import { getFee } from '../../background';
import { decryptResource, getGroupAdmins, validateSecretKey } from '../Group/Group'; import {
decryptResource,
getGroupAdmins,
validateSecretKey,
} from '../Group/Group';
import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; import { base64ToUint8Array } from '../../qdn/encryption/group-encryption';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup, setIsForceShowCreationKeyPopup, isForceShowCreationKeyPopup}) => { export const CreateCommonSecret = ({
groupId,
secretKey,
isOwner,
myAddress,
secretKeyDetails,
userInfo,
noSecretKey,
setHideCommonKeyPopup,
setIsForceShowCreationKeyPopup,
isForceShowCreationKeyPopup,
}) => {
const { show, setTxList } = useContext(MyContext); const { show, setTxList } = useContext(MyContext);
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null); const [infoSnack, setInfoSnack] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false);
const theme = useTheme();
const getPublishesFromAdmins = async (admins: string[]) => { const getPublishesFromAdmins = async (admins: string[]) => {
// const validApi = await findUsableApi(); // const validApi = await findUsableApi();
const queryString = admins.map((name) => `name=${name}`).join("&"); const queryString = admins.map((name) => `name=${name}`).join('&');
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${
groupId groupId
}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; }&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
const response = await fetch(url); const response = await fetch(url);
if(!response.ok){ if (!response.ok) {
throw new Error('network error') throw new Error('network error');
} }
const adminData = await response.json(); const adminData = await response.json();
const filterId = adminData.filter( const filterId = adminData.filter(
(data: any) => (data: any) => data.identifier === `symmetric-qchat-group-${groupId}`
data.identifier === `symmetric-qchat-group-${groupId}`
); );
if (filterId?.length === 0) { if (filterId?.length === 0) {
return false; return false;
@ -38,149 +59,182 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec
// Get the most recent date for both a and b // Get the most recent date for both a and b
const dateA = a.updated ? new Date(a.updated) : new Date(a.created); const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
const dateB = b.updated ? new Date(b.updated) : new Date(b.created); const dateB = b.updated ? new Date(b.updated) : new Date(b.created);
// Sort by most recent // Sort by most recent
return dateB.getTime() - dateA.getTime(); return dateB.getTime() - dateA.getTime();
}); });
return sortedData[0]; return sortedData[0];
}; };
const getSecretKey = async (loadingGroupParam?: boolean, secretKeyToPublish?: boolean) => {
const getSecretKey = async (
loadingGroupParam?: boolean,
secretKeyToPublish?: boolean
) => {
try { try {
pauseAllQueues() pauseAllQueues();
const { names } = await getGroupAdmins(groupId);
if (!names.length) {
const {names} = await getGroupAdmins(groupId); throw new Error('Network error');
if(!names.length){
throw new Error('Network error')
} }
const publish = await getPublishesFromAdmins(names); const publish = await getPublishesFromAdmins(names);
if (publish === false) { if (publish === false) {
return false; return false;
} }
const res = await fetch( const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
publish.identifier publish.identifier
}?encoding=base64&rebuild=true` }?encoding=base64&rebuild=true`
); );
const data = await res.text(); const data = await res.text();
const decryptedKey: any = await decryptResource(data); const decryptedKey: any = await decryptResource(data);
const dataint8Array = base64ToUint8Array(decryptedKey.data); const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
if (!validateSecretKey(decryptedKeyToObject)) if (!validateSecretKey(decryptedKeyToObject))
throw new Error("SecretKey is not valid"); throw new Error('SecretKey is not valid');
if (decryptedKeyToObject) { if (decryptedKeyToObject) {
return decryptedKeyToObject; return decryptedKeyToObject;
} else {
} }
} catch (error) { } catch (error) {
console.log(error);
} finally {
} }
}; };
const createCommonSecret = async ()=> { const createCommonSecret = async () => {
try { try {
const fee = await getFee('ARBITRARY') const fee = await getFee('ARBITRARY');
await show({ await show({
message: "Would you like to perform an ARBITRARY transaction?" , message: 'Would you like to perform an ARBITRARY transaction?',
publishFee: fee.fee + ' QORT' publishFee: fee.fee + ' QORT',
}) });
setIsLoading(true) setIsLoading(true);
const secretKey2 = await getSecretKey() const secretKey2 = await getSecretKey();
if((!secretKey2 && secretKey2 !== false)) throw new Error('invalid secret key') if (!secretKey2 && secretKey2 !== false)
if (secretKey2 && !validateSecretKey(secretKey2)) throw new Error('invalid secret key') throw new Error('invalid secret key');
if (secretKey2 && !validateSecretKey(secretKey2))
throw new Error('invalid secret key');
const secretKeyToSend = !secretKey2 ? null : secretKey2 const secretKeyToSend = !secretKey2 ? null : secretKey2;
window
window.sendMessage("encryptAndPublishSymmetricKeyGroupChat", { .sendMessage('encryptAndPublishSymmetricKeyGroupChat', {
groupId: groupId, groupId: groupId,
previousData: secretKeyToSend, previousData: secretKeyToSend,
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
setInfoSnack({ setInfoSnack({
type: "success", type: 'success',
message: "Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.", message:
}); 'Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.',
setOpenSnack(true);
setTxList((prev) => [
{
...response,
type: 'created-common-secret',
label: `Published secret key for group ${groupId}: awaiting confirmation`,
labelDone: `Published secret key for group ${groupId}: success!`,
done: false,
groupId,
},
...prev,
]);
}
setIsLoading(false);
setTimeout(() => {
setIsForceShowCreationKeyPopup(false)
}, 1000);
})
.catch((error) => {
console.error("Failed to encrypt and publish symmetric key for group chat:", error.message || "An error occurred");
setIsLoading(false);
}); });
setOpenSnack(true);
} catch (error) { setTxList((prev) => [
{
} ...response,
type: 'created-common-secret',
label: `Published secret key for group ${groupId}: awaiting confirmation`,
labelDone: `Published secret key for group ${groupId}: success!`,
done: false,
groupId,
},
...prev,
]);
}
setIsLoading(false);
setTimeout(() => {
setIsForceShowCreationKeyPopup(false);
}, 1000);
})
.catch((error) => {
console.error(
'Failed to encrypt and publish symmetric key for group chat:',
error.message || 'An error occurred'
);
setIsLoading(false);
});
} catch (error) {
console.log(error);
} }
};
return ( return (
<Box sx={{ <Box
padding: '25px', sx={{
display: 'flex', background: theme.palette.background.default,
flexDirection: 'column',
gap: '25px',
maxWidth: '350px',
background: '#444444'
}}>
<LoadingButton loading={isLoading} loadingPosition="start" color="warning" variant='contained' onClick={createCommonSecret}>Re-encrypt key</LoadingButton>
{noSecretKey ? (
<Box>
<Typography>There is no group secret key. Be the first admin to publish one!</Typography>
</Box>
) : isOwner && secretKeyDetails && userInfo?.name && userInfo.name !== secretKeyDetails?.name ? (
<Box>
<Typography>The latest group secret key was published by a non-owner. As the owner of the group please re-encrypt the key as a safeguard</Typography>
</Box>
): isForceShowCreationKeyPopup ? null : (
<Box>
<Typography>The group member list has changed. Please re-encrypt the secret key.</Typography>
</Box>
)}
<Box sx={{
display: 'flex', display: 'flex',
width: '100%', flexDirection: 'column',
justifyContent: 'flex-end' gap: '25px',
}}> maxWidth: '350px',
<Button onClick={()=> { padding: '25px',
setHideCommonKeyPopup(true) }}
setIsForceShowCreationKeyPopup(false) >
}} size='small'>Hide</Button> <LoadingButton
loading={isLoading}
loadingPosition="start"
color="warning"
variant="contained"
onClick={createCommonSecret}
>
Re-encrypt key
</LoadingButton>
{noSecretKey ? (
<Box>
<Typography>
There is no group secret key. Be the first admin to publish one!
</Typography>
</Box>
) : isOwner &&
secretKeyDetails &&
userInfo?.name &&
userInfo.name !== secretKeyDetails?.name ? (
<Box>
<Typography>
The latest group secret key was published by a non-owner. As the
owner of the group please re-encrypt the key as a safeguard
</Typography>
</Box>
) : isForceShowCreationKeyPopup ? null : (
<Box>
<Typography>
The group member list has changed. Please re-encrypt the secret key.
</Typography>
</Box>
)}
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
width: '100%',
}}
>
<Button
onClick={() => {
setHideCommonKeyPopup(true);
setIsForceShowCreationKeyPopup(false);
}}
size="small"
>
Hide
</Button>
</Box> </Box>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
<CustomizedSnackbars
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
</Box> </Box>
);
) };
}

View File

@ -5,31 +5,22 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { CreateCommonSecret } from './CreateCommonSecret';
import { reusableGet } from '../../qdn/publish/pubish';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
import { import {
base64ToUint8Array, base64ToUint8Array,
objectToBase64, objectToBase64,
} from '../../qdn/encryption/group-encryption'; } from '../../qdn/encryption/group-encryption';
import { ChatContainerComp } from './ChatContainer';
import { ChatList } from './ChatList';
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css'; import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
import Tiptap from './TipTap'; import Tiptap from './TipTap';
import { import { CustomButton } from '../../styles/App-styles';
AuthenticatedContainerInnerTop,
CustomButton,
} from '../../styles/App-styles';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { getBaseApi, getFee } from '../../background'; import { getFee } from '../../background';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
import { Box, Typography } from '@mui/material'; import { Box, Typography, useTheme } from '@mui/material';
import { Spacer } from '../../common/Spacer'; import { Spacer } from '../../common/Spacer';
import ShortUniqueId from 'short-unique-id'; import ShortUniqueId from 'short-unique-id';
import { AnnouncementList } from './AnnouncementList'; import { AnnouncementList } from './AnnouncementList';
const uid = new ShortUniqueId({ length: 8 });
import CampaignIcon from '@mui/icons-material/Campaign'; import CampaignIcon from '@mui/icons-material/Campaign';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { AnnouncementDiscussion } from './AnnouncementDiscussion'; import { AnnouncementDiscussion } from './AnnouncementDiscussion';
import { import {
MyContext, MyContext,
@ -42,9 +33,11 @@ import {
import { RequestQueueWithPromise } from '../../utils/queue/queue'; import { RequestQueueWithPromise } from '../../utils/queue/queue';
import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { addDataPublishesFunc, getDataPublishesFunc } from '../Group/Group'; import { addDataPublishesFunc, getDataPublishesFunc } from '../Group/Group';
import { getRootHeight } from '../../utils/mobile/mobileUtils';
const uid = new ShortUniqueId({ length: 8 });
export const requestQueueCommentCount = new RequestQueueWithPromise(3); export const requestQueueCommentCount = new RequestQueueWithPromise(3);
export const requestQueuePublishedAccouncements = new RequestQueueWithPromise( export const requestQueuePublishedAccouncements = new RequestQueueWithPromise(
3 3
); );
@ -125,6 +118,7 @@ export const handleUnencryptedPublishes = (publishes) => {
}); });
return publishesData; return publishesData;
}; };
export const GroupAnnouncements = ({ export const GroupAnnouncements = ({
selectedGroup, selectedGroup,
secretKey, secretKey,
@ -264,6 +258,7 @@ export const GroupAnnouncements = ({
}); });
}); });
}; };
const clearEditorContent = () => { const clearEditorContent = () => {
if (editorRef.current) { if (editorRef.current) {
editorRef.current.chain().focus().clearContent().run(); editorRef.current.chain().focus().clearContent().run();
@ -301,10 +296,12 @@ export const GroupAnnouncements = ({
try { try {
pauseAllQueues(); pauseAllQueues();
const fee = await getFee('ARBITRARY'); const fee = await getFee('ARBITRARY');
await show({ await show({
message: 'Would you like to perform a ARBITRARY transaction?', message: 'Would you like to perform a ARBITRARY transaction?',
publishFee: fee.fee + ' QORT', publishFee: fee.fee + ' QORT',
}); });
if (isSending) return; if (isSending) return;
if (editorRef.current) { if (editorRef.current) {
const htmlContent = editorRef.current.getHTML(); const htmlContent = editorRef.current.getHTML();
@ -387,8 +384,7 @@ export const GroupAnnouncements = ({
); );
} }
} catch (error) { } catch (error) {
} finally { console.log(error);
// dispatch(setIsLoadingGlobal(false))
} }
}, },
[secretKey] [secretKey]
@ -437,6 +433,8 @@ export const GroupAnnouncements = ({
const interval = useRef<any>(null); const interval = useRef<any>(null);
const theme = useTheme();
const checkNewMessages = React.useCallback(async () => { const checkNewMessages = React.useCallback(async () => {
try { try {
const identifier = `grp-${selectedGroup}-anc-`; const identifier = `grp-${selectedGroup}-anc-`;
@ -485,7 +483,7 @@ export const GroupAnnouncements = ({
} }
setAnnouncements((prev) => [...newArray, ...prev]); setAnnouncements((prev) => [...newArray, ...prev]);
} catch (error) { } catch (error) {
} finally { console.log(error);
} }
}, [announcements, secretKey, selectedGroup]); }, [announcements, secretKey, selectedGroup]);
@ -537,10 +535,10 @@ export const GroupAnnouncements = ({
: 'calc(100vh - 70px)', : 'calc(100vh - 70px)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
width: '100%',
visibility: hide && 'hidden',
position: hide && 'fixed',
left: hide && '-1000px', left: hide && '-1000px',
position: hide && 'fixed',
visibility: hide && 'hidden',
width: '100%',
}} }}
> >
<AnnouncementDiscussion <AnnouncementDiscussion
@ -560,54 +558,54 @@ export const GroupAnnouncements = ({
return ( return (
<div <div
style={{ style={{
// reference to change height
height: isMobile ? `calc(${rootHeight} - 127px` : 'calc(100vh - 70px)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
width: '100%', height: 'calc(100vh - 70px)',
visibility: hide && 'hidden',
position: hide && 'fixed',
left: hide && '-1000px', left: hide && '-1000px',
position: hide && 'fixed',
visibility: hide && 'hidden',
width: '100%',
}} }}
> >
<div <div
style={{ style={{
position: 'relative',
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexShrink: 0, flexShrink: 0,
position: 'relative',
width: '100%',
}} }}
> >
{!isMobile && ( {!isMobile && (
<Box <Box
sx={{ sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
padding: isMobile ? '8px' : '25px',
fontSize: isMobile ? '16px' : '20px',
gap: '20px',
alignItems: 'center', alignItems: 'center',
display: 'flex',
fontSize: '20px',
gap: '20px',
justifyContent: 'center',
padding: '25px',
width: '100%',
}} }}
> >
<CampaignIcon <CampaignIcon
sx={{ sx={{
fontSize: isMobile ? '16px' : '30px', fontSize: '30px',
}} }}
/> />
Group Announcements Group Announcements
</Box> </Box>
)} )}
<Spacer height={isMobile ? '0px' : '25px'} /> <Spacer height={'25px'} />
</div> </div>
{!isLoading && combinedListTempAndReal?.length === 0 && ( {!isLoading && combinedListTempAndReal?.length === 0 && (
<Box <Box
sx={{ sx={{
width: '100%',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
width: '100%',
}} }}
> >
<Typography <Typography
@ -634,31 +632,28 @@ export const GroupAnnouncements = ({
{isAdmin && ( {isAdmin && (
<div <div
style={{ style={{
// position: 'fixed', backgroundColor: theme.palette.background.default,
// bottom: '0px', bottom: isFocusedParent ? '0px' : 'unset',
backgroundColor: '#232428', boxSizing: 'border-box',
minHeight: isMobile ? '0px' : '150px',
maxHeight: isMobile ? 'auto' : '400px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
padding: isMobile ? '10px' : '20px',
position: isFocusedParent ? 'fixed' : 'relative',
bottom: isFocusedParent ? '0px' : 'unset',
top: isFocusedParent ? '0px' : 'unset',
zIndex: isFocusedParent ? 5 : 'unset',
flexShrink: 0, flexShrink: 0,
maxHeight: '400px',
minHeight: '150px',
overflow: 'hidden',
padding: '20px',
position: isFocusedParent ? 'fixed' : 'relative',
top: isFocusedParent ? '0px' : 'unset',
width: '100%',
zIndex: isFocusedParent ? 5 : 'unset',
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: isMobile && 1, flexGrow: 1,
overflow: 'auto', overflow: 'auto',
// height: '100%',
}} }}
> >
<Tiptap <Tiptap
@ -670,14 +665,15 @@ export const GroupAnnouncements = ({
setIsFocusedParent={setIsFocusedParent} setIsFocusedParent={setIsFocusedParent}
/> />
</div> </div>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100&', flexShrink: 0,
gap: '10px', gap: '10px',
justifyContent: 'center', justifyContent: 'center',
flexShrink: 0,
position: 'relative', position: 'relative',
width: '100&',
}} }}
> >
{isFocusedParent && ( {isFocusedParent && (
@ -692,43 +688,46 @@ export const GroupAnnouncements = ({
// Unfocus the editor // Unfocus the editor
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
cursor: isSending ? 'default' : 'pointer',
background: 'var(--danger)', background: 'var(--danger)',
cursor: isSending ? 'default' : 'pointer',
flexShrink: 0, flexShrink: 0,
padding: isMobile && '5px', fontSize: '14px',
fontSize: isMobile && '14px', marginTop: 'auto',
padding: '5px',
}} }}
> >
{` Close`} {` Close`}
</CustomButton> </CustomButton>
)} )}
<CustomButton <CustomButton
onClick={() => { onClick={() => {
if (isSending) return; if (isSending) return;
publishAnnouncement(); publishAnnouncement();
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
background: isSending
? theme.palette.background.default
: theme.palette.background.paper,
cursor: isSending ? 'default' : 'pointer', cursor: isSending ? 'default' : 'pointer',
background: isSending && 'rgba(0, 0, 0, 0.8)',
flexShrink: 0, flexShrink: 0,
padding: isMobile && '5px', fontSize: '14px',
fontSize: isMobile && '14px', marginTop: 'auto',
padding: '5px',
}} }}
> >
{isSending && ( {isSending && (
<CircularProgress <CircularProgress
size={18} size={18}
sx={{ sx={{
color: theme.palette.text.primary,
left: '50%',
marginLeft: '-12px',
marginTop: '-12px',
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
color: 'white',
}} }}
/> />
)} )}

View File

@ -1,19 +1,6 @@
import React, { import { useContext, useEffect, useState } from 'react';
useCallback, import { GroupMail } from '../Group/Forum/GroupMail';
useContext, import { MyContext, isMobile } from '../../App';
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { GroupMail } from "../Group/Forum/GroupMail";
import { MyContext, isMobile } from "../../App";
import { getRootHeight } from "../../utils/mobile/mobileUtils";
export const GroupForum = ({ export const GroupForum = ({
selectedGroup, selectedGroup,
@ -23,12 +10,13 @@ export const GroupForum = ({
isAdmin, isAdmin,
myAddress, myAddress,
hide, hide,
defaultThread, defaultThread,
setDefaultThread, setDefaultThread,
isPrivate isPrivate,
}) => { }) => {
const { rootHeight } = useContext(MyContext); const { rootHeight } = useContext(MyContext);
const [isMoved, setIsMoved] = useState(false); const [isMoved, setIsMoved] = useState(false);
useEffect(() => { useEffect(() => {
if (hide) { if (hide) {
setTimeout(() => setIsMoved(true), 300); // Wait for the fade-out to complete before moving setTimeout(() => setIsMoved(true), 300); // Wait for the fade-out to complete before moving
@ -39,20 +27,27 @@ export const GroupForum = ({
return ( return (
<div <div
style={{ style={{
// reference to change height display: 'flex',
height: isMobile ? `calc(${rootHeight} - 127px` : "calc(100vh - 70px)", flexDirection: 'column',
display: "flex", height: 'calc(100vh - 70px)',
flexDirection: "column", left: hide && '-1000px',
width: "100%", opacity: hide ? 0 : 1,
opacity: hide ? 0 : 1, position: hide ? 'fixed' : 'relative',
visibility: hide && 'hidden', visibility: hide && 'hidden',
position: hide ? 'fixed' : 'relative', width: '100%',
left: hide && '-1000px' }}
}} >
> <GroupMail
<GroupMail isPrivate={isPrivate} hide={hide} getSecretKey={getSecretKey} selectedGroup={selectedGroup} userInfo={userInfo} secretKey={secretKey} defaultThread={defaultThread} setDefaultThread={setDefaultThread} /> isPrivate={isPrivate}
hide={hide}
</div> getSecretKey={getSecretKey}
selectedGroup={selectedGroup}
userInfo={userInfo}
secretKey={secretKey}
defaultThread={defaultThread}
setDefaultThread={setDefaultThread}
/>
</div>
); );
}; };

View File

@ -1,69 +1,68 @@
import React, { import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
forwardRef, useEffect, useImperativeHandle,
useState, export default forwardRef((props, ref) => {
} from 'react' const [selectedIndex, setSelectedIndex] = useState(0);
export default forwardRef((props, ref) => { const selectItem = (index) => {
const [selectedIndex, setSelectedIndex] = useState(0) const item = props.items[index];
const selectItem = index => { if (item) {
const item = props.items[index] props.command(item);
}
if (item) { };
props.command(item)
const upHandler = () => {
setSelectedIndex(
(selectedIndex + props.items.length - 1) % props.items.length
);
};
const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};
const enterHandler = () => {
selectItem(selectedIndex);
};
useEffect(() => setSelectedIndex(0), [props.items]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
upHandler();
return true;
} }
}
if (event.key === 'ArrowDown') {
const upHandler = () => { downHandler();
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length) return true;
} }
const downHandler = () => { if (event.key === 'Enter') {
setSelectedIndex((selectedIndex + 1) % props.items.length) enterHandler();
} return true;
}
const enterHandler = () => {
selectItem(selectedIndex) return false;
} },
}));
useEffect(() => setSelectedIndex(0), [props.items])
return (
useImperativeHandle(ref, () => ({ <div className="dropdown-menu">
onKeyDown: ({ event }) => { {props.items.length ? (
if (event.key === 'ArrowUp') { props.items.map((item, index) => (
upHandler() <button
return true className={index === selectedIndex ? 'is-selected' : ''}
} key={item.id || index}
onClick={() => selectItem(index)}
if (event.key === 'ArrowDown') { >
downHandler() {item.label}
return true </button>
} ))
) : (
if (event.key === 'Enter') { <div className="item">No result</div>
enterHandler() )}
return true </div>
} );
});
return false
},
}))
return (
<div className="dropdown-menu">
{props.items.length
? props.items.map((item, index) => (
<button
className={index === selectedIndex ? 'is-selected' : ''}
key={item.id || index}
onClick={() => selectItem(index)}
>
{item.label}
</button>
))
: <div className="item">No result</div>
}
</div>
)
})

View File

@ -1,8 +1,10 @@
import React, { useRef } from 'react'; import { useRef } from 'react';
import { NodeViewWrapper } from '@tiptap/react'; import { NodeViewWrapper } from '@tiptap/react';
import { useTheme } from '@mui/material';
const ResizableImage = ({ node, updateAttributes, selected }) => { const ResizableImage = ({ node, updateAttributes, selected }) => {
const imgRef = useRef(null); const imgRef = useRef(null);
const theme = useTheme();
const startResizing = (e) => { const startResizing = (e) => {
e.preventDefault(); e.preventDefault();
@ -40,18 +42,23 @@ const ResizableImage = ({ node, updateAttributes, selected }) => {
src={node.attrs.src} src={node.attrs.src}
alt={node.attrs.alt || ''} alt={node.attrs.alt || ''}
title={node.attrs.title || ''} title={node.attrs.title || ''}
style={{ width: node.attrs.width || 'auto', display: 'block', margin: '0 auto' }} style={{
width: node.attrs.width || 'auto',
display: 'block',
margin: '0 auto',
}}
draggable={false} // Prevent image dragging draggable={false} // Prevent image dragging
/> />
<div <div
style={{ style={{
backgroundColor: theme.palette.background.paper,
bottom: 0,
cursor: 'nwse-resize',
height: '10px',
position: 'absolute', position: 'absolute',
right: 0, right: 0,
bottom: 0,
width: '10px', width: '10px',
height: '10px',
backgroundColor: 'gray',
cursor: 'nwse-resize',
zIndex: 1, // Ensure the resize handle is above other content zIndex: 1, // Ensure the resize handle is above other content
}} }}
onMouseDown={startResizing} onMouseDown={startResizing}