version 2 - beta

This commit is contained in:
2024-09-09 20:36:39 +03:00
parent c2fcfaeaed
commit f75efc8cf6
99 changed files with 20541 additions and 757 deletions

View File

@@ -0,0 +1,474 @@
import * as React from "react";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import ListItemText from "@mui/material/ListItemText";
import ListItemButton from "@mui/material/ListItemButton";
import List from "@mui/material/List";
import Divider from "@mui/material/Divider";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import CloseIcon from "@mui/icons-material/Close";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
import Slide from "@mui/material/Slide";
import { TransitionProps } from "@mui/material/transitions";
import {
Box,
Collapse,
Input,
MenuItem,
Select,
SelectChangeEvent,
Tab,
Tabs,
styled,
} from "@mui/material";
import { AddGroupList } from "./AddGroupList";
import { UserListOfInvites } from "./UserListOfInvites";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { getFee } from "../../background";
import { MyContext } from "../../App";
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
export const Label = styled("label")(
({ theme }) => `
font-family: 'IBM Plex Sans', sans-serif;
font-size: 14px;
display: block;
margin-bottom: 4px;
font-weight: 400;
`
);
const Transition = React.forwardRef(function Transition(
props: TransitionProps & {
children: React.ReactElement;
},
ref: React.Ref<unknown>
) {
return <Slide direction="up" ref={ref} {...props} />;
});
export const AddGroup = ({ address, open, setOpen }) => {
const {show, setTxList} = React.useContext(MyContext)
const [tab, setTab] = React.useState("create");
const [openAdvance, setOpenAdvance] = React.useState(false);
const [name, setName] = React.useState("");
const [description, setDescription] = React.useState("");
const [groupType, setGroupType] = React.useState("1");
const [approvalThreshold, setApprovalThreshold] = React.useState("40");
const [minBlock, setMinBlock] = React.useState("5");
const [maxBlock, setMaxBlock] = React.useState("21600");
const [value, setValue] = React.useState(0);
const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null);
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};
const handleClose = () => {
setOpen(false);
};
const handleChangeGroupType = (event: SelectChangeEvent) => {
setGroupType(event.target.value as string);
};
const handleChangeApprovalThreshold = (event: SelectChangeEvent) => {
setGroupType(event.target.value as string);
};
const handleChangeMinBlock = (event: SelectChangeEvent) => {
setMinBlock(event.target.value as string);
};
const handleChangeMaxBlock = (event: SelectChangeEvent) => {
setMaxBlock(event.target.value as string);
};
const handleCreateGroup = async () => {
try {
if(!name) throw new Error('Please provide a name')
if(!description) throw new Error('Please provide a description')
const fee = await getFee('CREATE_GROUP')
await show({
message: "Would you like to perform an CREATE_GROUP transaction?" ,
publishFee: fee.fee + ' QORT'
})
await new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "createGroup",
payload: {
groupName: name,
groupDescription: description,
groupType: +groupType,
groupApprovalThreshold: +approvalThreshold,
minBlock: +minBlock,
maxBlock: +maxBlock,
},
},
(response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: "Successfully created group. It may take a couple of minutes for the changes to propagate",
});
setOpenSnack(true);
setTxList((prev)=> [{
...response,
type: 'created-group',
label: `Created group ${name}: awaiting confirmation`,
labelDone: `Created group ${name}: success !`,
done: false
}, ...prev])
res(response);
return
}
rej({message: response.error});
}
);
});
} catch (error) {
setInfoSnack({
type: "error",
message: error?.message,
});
setOpenSnack(true);
}
};
function CustomTabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
}
const openGroupInvitesRequestFunc = ()=> {
setValue(2)
}
React.useEffect(() => {
subscribeToEvent("openGroupInvitesRequest", openGroupInvitesRequestFunc);
return () => {
unsubscribeFromEvent("openGroupInvitesRequest", openGroupInvitesRequestFunc);
};
}, []);
return (
<React.Fragment>
<Dialog
fullScreen
open={open}
onClose={handleClose}
TransitionComponent={Transition}
>
<AppBar sx={{ position: "relative", bgcolor: "#232428" }}>
<Toolbar>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
Add Group
</Typography>
<IconButton
edge="start"
color="inherit"
onClick={handleClose}
aria-label="close"
>
<CloseIcon />
</IconButton>
{/* <Button autoFocus color="inherit" onClick={handleClose}>
save
</Button> */}
</Toolbar>
</AppBar>
<Box
sx={{
bgcolor: "#27282c",
flexGrow: 1,
overflowY: "auto",
color: "white",
}}
>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
sx={{
"& .MuiTabs-indicator": {
backgroundColor: "white",
},
}}
value={value}
onChange={handleChange}
aria-label="basic tabs example"
>
<Tab
sx={{
"&.Mui-selected": {
color: `white`,
},
}}
label="Create Group"
{...a11yProps(0)}
/>
<Tab
sx={{
"&.Mui-selected": {
color: `white`,
},
}}
label="Find Group"
{...a11yProps(1)}
/>
<Tab
sx={{
"&.Mui-selected": {
color: `white`,
},
}}
label="Group Invites"
{...a11yProps(2)}
/>
</Tabs>
</Box>
{value === 0 && (
<Box sx={{
width: '100%',
padding: '25px'
}}>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "20px",
maxWidth: "500px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>Name of group</Label>
<Input
placeholder="Name of group"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>Description of group</Label>
<Input
placeholder="Description of group"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>Group type</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={groupType}
label="Group Type"
onChange={handleChangeGroupType}
>
<MenuItem value={1}>Open (public)</MenuItem>
<MenuItem value={0}>
Closed (private) - users need permission to join
</MenuItem>
</Select>
</Box>
<Box
sx={{
display: "flex",
gap: "15px",
alignItems: "center",
cursor: "pointer",
}}
onClick={() => setOpenAdvance((prev) => !prev)}
>
<Typography>Advanced options</Typography>
{openAdvance ? <ExpandLess /> : <ExpandMore />}
</Box>
<Collapse in={openAdvance} timeout="auto" unmountOnExit>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>
Group Approval Threshold (number / percentage of Admins that
must approve a transaction)
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={approvalThreshold}
label="Group Approval Threshold"
onChange={handleChangeApprovalThreshold}
>
<MenuItem value={0}>NONE</MenuItem>
<MenuItem value={1}>ONE </MenuItem>
<MenuItem value={20}>20% </MenuItem>
<MenuItem value={40}>40% </MenuItem>
<MenuItem value={60}>60% </MenuItem>
<MenuItem value={80}>80% </MenuItem>
<MenuItem value={100}>100% </MenuItem>
</Select>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>
Minimum Block delay for Group Transaction Approvals
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={minBlock}
label="Minimum Block delay"
onChange={handleChangeMinBlock}
>
<MenuItem value={5}>5 minutes</MenuItem>
<MenuItem value={10}>10 minutes</MenuItem>
<MenuItem value={30}>30 minutes</MenuItem>
<MenuItem value={60}>1 hour</MenuItem>
<MenuItem value={180}>3 hours</MenuItem>
<MenuItem value={300}>5 hours</MenuItem>
<MenuItem value={420}>7 hours</MenuItem>
<MenuItem value={720}>12 hours</MenuItem>
<MenuItem value={1440}>1 day</MenuItem>
<MenuItem value={4320}>3 days</MenuItem>
<MenuItem value={7200}>5 days</MenuItem>
<MenuItem value={10080}>7 days</MenuItem>
</Select>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>
Maximum Block delay for Group Transaction Approvals
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={maxBlock}
label="Maximum Block delay"
onChange={handleChangeMaxBlock}
>
<MenuItem value={60}>1 hour</MenuItem>
<MenuItem value={180}>3 hours</MenuItem>
<MenuItem value={300}>5 hours</MenuItem>
<MenuItem value={420}>7 hours</MenuItem>
<MenuItem value={720}>12 hours</MenuItem>
<MenuItem value={1440}>1 day</MenuItem>
<MenuItem value={4320}>3 days</MenuItem>
<MenuItem value={7200}>5 days</MenuItem>
<MenuItem value={10080}>7 days</MenuItem>
<MenuItem value={14400}>10 days</MenuItem>
<MenuItem value={21600}>15 days</MenuItem>
</Select>
</Box>
</Collapse>
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<Button
variant="contained"
color="primary"
onClick={handleCreateGroup}
>
Create Group
</Button>
</Box>
</Box>
</Box>
)}
{value === 1 && (
<Box sx={{
width: '100%',
padding: '25px'
}}>
<AddGroupList setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} />
</Box>
)}
{value === 2 && (
<Box sx={{
width: '100%',
padding: '25px'
}}>
<UserListOfInvites myAddress={address} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} />
</Box>
)}
</Box>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
</Dialog>
</React.Fragment>
);
};

View File

@@ -0,0 +1,272 @@
import {
Box,
Button,
ListItem,
ListItemButton,
ListItemText,
Popover,
TextField,
Typography,
} from "@mui/material";
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List,
} from "react-virtualized";
import _ from "lodash";
import { MyContext, getBaseApiReact } from "../../App";
import { LoadingButton } from "@mui/lab";
import { getBaseApi, getFee } from "../../background";
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
const { memberGroups, show, setTxList } = useContext(MyContext);
const [groups, setGroups] = useState([]);
const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to
const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open
const listRef = useRef();
const [inputValue, setInputValue] = useState("");
const [filteredItems, setFilteredItems] = useState(groups);
const [isLoading, setIsLoading] = useState(false);
const handleFilter = useCallback(
(query) => {
if (query) {
setFilteredItems(
groups.filter((item) =>
item.groupName.toLowerCase().includes(query.toLowerCase())
)
);
} else {
setFilteredItems(groups);
}
},
[groups]
);
const debouncedFilter = useMemo(
() => _.debounce(handleFilter, 500),
[handleFilter]
);
const handleChange = (event) => {
const value = event.target.value;
setInputValue(value);
debouncedFilter(value);
};
const getGroups = async () => {
try {
const response = await fetch(
`${getBaseApiReact()}/groups/?limit=0`
);
const groupData = await response.json();
const filteredGroup = groupData.filter(
(item) => !memberGroups.find((group) => group.groupId === item.groupId)
);
setGroups(filteredGroup);
setFilteredItems(filteredGroup);
} catch (error) {
console.error(error);
}
};
useEffect(() => {
getGroups();
}, [memberGroups]);
const handlePopoverOpen = (event, index) => {
setPopoverAnchor(event.currentTarget);
setOpenPopoverIndex(index);
};
const handlePopoverClose = () => {
setPopoverAnchor(null);
setOpenPopoverIndex(null);
};
const handleJoinGroup = async (group, isOpen) => {
try {
const groupId = group.groupId;
const fee = await getFee('JOIN_GROUP')
await show({
message: "Would you like to perform an JOIN_GROUP transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoading(true);
await new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "joinGroup",
payload: {
groupId,
},
},
(response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: "Successfully requested to join group. It may take a couple of minutes for the changes to propagate",
});
if(isOpen){
setTxList((prev)=> [{
...response,
type: 'joined-group',
label: `Joined Group ${group?.groupName}: awaiting confirmation`,
labelDone: `Joined Group ${group?.groupName}: success !`,
done: false,
groupId,
}, ...prev])
} else {
setTxList((prev)=> [{
...response,
type: 'joined-group-request',
label: `Requested to join Group ${group?.groupName}: awaiting confirmation`,
labelDone: `Requested to join Group ${group?.groupName}: success !`,
done: false,
groupId,
}, ...prev])
}
setOpenSnack(true);
handlePopoverClose();
res(response);
return;
} else {
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error);
}
}
);
});
setIsLoading(false);
} catch (error) {} finally {
setIsLoading(false);
}
};
const rowRenderer = ({ index, key, parent, style }) => {
const group = filteredItems[index];
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
{({ measure }) => (
<div style={style} onLoad={measure}>
<ListItem disablePadding>
<Popover
open={openPopoverIndex === index}
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
style={{ marginTop: "8px" }}
>
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<Typography>Join {group?.groupName}</Typography>
<Typography>
{group?.isOpen === false &&
"This is a closed/private group, so you will need to wait until an admin accepts your request"}
</Typography>
<LoadingButton
loading={isLoading}
loadingPosition="start"
variant="contained"
onClick={() => handleJoinGroup(group, group?.isOpen)}
>
Join group
</LoadingButton>
</Box>
</Popover>
<ListItemButton
onClick={(event) => handlePopoverOpen(event, index)}
>
<ListItemText
primary={group?.groupName}
secondary={group?.description}
/>
</ListItemButton>
</ListItem>
</div>
)}
</CellMeasurer>
);
};
return (
<div>
<p>Groups list</p>
<TextField
label="Search for Groups"
variant="outlined"
fullWidth
value={inputValue}
onChange={handleChange}
/>
<div
style={{
position: "relative",
height: "500px",
width: "600px",
display: "flex",
flexDirection: "column",
flexShrink: 1,
}}
>
<AutoSizer>
{({ height, width }) => (
<List
ref={listRef}
width={width}
height={height}
rowCount={filteredItems.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
deferredMeasurementCache={cache}
/>
)}
</AutoSizer>
</div>
</div>
);
};

View File

@@ -0,0 +1,45 @@
import { useMemo } from "react";
import DOMPurify from "dompurify";
import "react-quill/dist/quill.snow.css";
import "react-quill/dist/quill.core.css";
import "react-quill/dist/quill.bubble.css";
import { Box, styled } from "@mui/material";
import { convertQortalLinks } from "../../../utils/qortalLink";
const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
display: "flex",
fontFamily: "Mulish",
fontSize: "19px",
fontWeight: 400,
letterSpacing: 0,
color: theme.palette.text.primary,
width: '100%'
}));
export const DisplayHtml = ({ html, textColor }: any) => {
const cleanContent = useMemo(() => {
if (!html) return null;
const sanitize: string = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
});
const anchorQortal = convertQortalLinks(sanitize);
return anchorQortal;
}, [html]);
if (!cleanContent) return null;
return (
<CrowdfundInlineContent>
<div
className="ql-editor-display"
style={{
color: textColor || 'white',
fontWeight: 400,
fontSize: '16px'
}}
dangerouslySetInnerHTML={{ __html: cleanContent }}
/>
</CrowdfundInlineContent>
);
};

View File

@@ -0,0 +1,777 @@
import React, {
FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Avatar, Box, Popover, Typography } from "@mui/material";
// import { MAIL_SERVICE_TYPE, THREAD_SERVICE_TYPE } from "../../constants/mail";
import { Thread } from "./Thread";
import {
AllThreadP,
ArrowDownIcon,
ComposeContainer,
ComposeContainerBlank,
ComposeIcon,
ComposeP,
GroupContainer,
GroupNameP,
InstanceFooter,
InstanceListContainer,
InstanceListContainerRow,
InstanceListContainerRowCheck,
InstanceListContainerRowCheckIcon,
InstanceListContainerRowMain,
InstanceListContainerRowMainP,
InstanceListHeader,
InstanceListParent,
SelectInstanceContainerFilterInner,
SingleThreadParent,
ThreadContainer,
ThreadContainerFullWidth,
ThreadInfoColumn,
ThreadInfoColumnNameP,
ThreadInfoColumnTime,
ThreadInfoColumnbyP,
ThreadSingleLastMessageP,
ThreadSingleLastMessageSpanP,
ThreadSingleTitle,
} from "./Mail-styles";
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import { Spacer } from "../../../common/Spacer";
import { formatDate, formatTimestamp } from "../../../utils/time";
import LazyLoad from "../../../common/LazyLoad";
import { delay } from "../../../utils/helpers";
import { NewThread } from "./NewThread";
import { getBaseApi } from "../../../background";
import { decryptPublishes, getTempPublish } from "../../Chat/GroupAnnouncements";
import CheckSVG from "../../../assets/svgs/Check.svg";
import SortSVG from "../../../assets/svgs/Sort.svg";
import ArrowDownSVG from "../../../assets/svgs/ArrowDown.svg";
import { LoadingSnackbar } from "../../Snackbar/LoadingSnackbar";
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../../utils/events";
import RefreshIcon from '@mui/icons-material/Refresh';
import { getBaseApiReact } from "../../../App";
const filterOptions = ["Recently active", "Newest", "Oldest"];
export const threadIdentifier = "DOCUMENT";
export const GroupMail = ({
selectedGroup,
userInfo,
getSecretKey,
secretKey,
defaultThread,
setDefaultThread
}) => {
const [viewedThreads, setViewedThreads] = React.useState<any>({});
const [filterMode, setFilterMode] = useState<string>("Recently active");
const [currentThread, setCurrentThread] = React.useState(null);
const [recentThreads, setRecentThreads] = useState<any[]>([]);
const [allThreads, setAllThreads] = useState<any[]>([]);
const [members, setMembers] = useState<any>(null);
const [isOpenFilterList, setIsOpenFilterList] = useState<boolean>(false);
const anchorElInstanceFilter = useRef<any>(null);
const [tempPublishedList, setTempPublishedList] = useState([])
const [isLoading, setIsLoading] = useState(false)
const groupIdRef = useRef<any>(null);
const groupId = useMemo(() => {
return selectedGroup?.groupId;
}, [selectedGroup]);
useEffect(() => {
if (groupId !== groupIdRef?.current) {
setCurrentThread(null);
setRecentThreads([]);
setAllThreads([]);
groupIdRef.current = groupId;
}
}, [groupId]);
const setTempData = async ()=> {
try {
const getTempAnnouncements = await getTempPublish()
if(getTempAnnouncements?.thread){
let tempData = []
Object.keys(getTempAnnouncements?.thread || {}).map((key)=> {
const value = getTempAnnouncements?.thread[key]
tempData.push(value.data)
})
setTempPublishedList(tempData)
}
} catch (error) {
}
}
const getEncryptedResource = async ({ name, identifier }) => {
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64`
);
const data = await res.text();
const response = await decryptPublishes([{ data }], secretKey);
const messageData = response[0];
return messageData.decryptedData;
};
const updateThreadActivity = async ({threadId, qortalName, groupId, thread}) => {
try {
await new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "updateThreadActivity",
payload: {
threadId, qortalName, groupId, thread
},
},
(response) => {
if (!response?.error) {
res(response);
return
}
rej(response.error);
}
);
});
} catch (error) {
} finally {
}
};
const getAllThreads = React.useCallback(
async (groupId: string, mode: string, isInitial?: boolean) => {
try {
setIsLoading(true)
const offset = isInitial ? 0 : allThreads.length;
const isReverse = mode === "Newest" ? true : false;
if (isInitial) {
// dispatch(setIsLoadingCustom("Loading threads"));
}
const identifier = `grp-${groupId}-thread-`;
const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=${20}&includemetadata=false&offset=${offset}&reverse=${isReverse}&prefix=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
let fullArrayMsg = isInitial ? [] : [...allThreads];
const getMessageForThreads = responseData.map(async (message: any) => {
let fullObject: any = null;
if (message?.metadata?.description) {
fullObject = {
...message,
threadData: {
title: message?.metadata?.description,
groupId: groupId,
createdAt: message?.created,
name: message?.name,
},
threadOwner: message?.name,
};
} else {
let threadRes = null;
try {
threadRes = await Promise.race([
getEncryptedResource({
name: message.name,
identifier: message.identifier,
}),
delay(5000),
]);
} catch (error) {}
if (threadRes?.title) {
fullObject = {
...message,
threadData: threadRes,
threadOwner: message?.name,
threadId: message.identifier
};
}
}
if (fullObject?.identifier) {
const index = fullArrayMsg.findIndex(
(p) => p.identifier === fullObject.identifier
);
if (index !== -1) {
fullArrayMsg[index] = fullObject;
} else {
fullArrayMsg.push(fullObject);
}
}
});
await Promise.all(getMessageForThreads);
let sorted = fullArrayMsg;
if (isReverse) {
sorted = fullArrayMsg.sort((a: any, b: any) => b.created - a.created);
} else {
sorted = fullArrayMsg.sort((a: any, b: any) => a.created - b.created);
}
setAllThreads(sorted);
} catch (error) {
console.log({ error });
} finally {
if (isInitial) {
setIsLoading(false)
// dispatch(setIsLoadingCustom(null));
}
}
},
[allThreads]
);
const getMailMessages = React.useCallback(
async (groupId: string, members: any) => {
try {
setIsLoading(true)
// const memberNames = Object.keys(members);
// const queryString = memberNames
// .map(name => `&name=${encodeURIComponent(name)}`)
// .join("");
// dispatch(setIsLoadingCustom("Loading recent threads"));
const identifier = `thmsg-grp-${groupId}-thread-`;
const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=100&includemetadata=false&offset=${0}&reverse=true&prefix=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
const messagesForThread: any = {};
for (const message of responseData) {
let str = message.identifier;
const parts = str.split("-");
// Get the second last element
const secondLastId = parts[parts.length - 2];
const result = `grp-${groupId}-thread-${secondLastId}`;
const checkMessage = messagesForThread[result];
if (!checkMessage) {
messagesForThread[result] = message;
}
}
const newArray = Object.keys(messagesForThread)
.map((key) => {
return {
...messagesForThread[key],
threadId: key,
};
})
.sort((a, b) => b.created - a.created)
.slice(0, 10);
let fullThreadArray: any = [];
const getMessageForThreads = newArray.map(async (message: any) => {
try {
const identifierQuery = message.threadId;
const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifierQuery}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
if (responseData.length > 0) {
const thread = responseData[0];
if (thread?.metadata?.description) {
const fullObject = {
...message,
threadData: {
title: thread?.metadata?.description,
groupId: groupId,
createdAt: thread?.created,
name: thread?.name,
},
threadOwner: thread?.name,
};
fullThreadArray.push(fullObject);
} else {
let threadRes = await Promise.race([
getEncryptedResource({
name: thread.name,
identifier: message.threadId,
}),
delay(10000),
]);
if (threadRes?.title) {
const fullObject = {
...message,
threadData: threadRes,
threadOwner: thread?.name,
};
fullThreadArray.push(fullObject);
}
}
}
} catch (error) {
console.log(error);
}
return null;
});
await Promise.all(getMessageForThreads);
const sorted = fullThreadArray.sort(
(a: any, b: any) => b.created - a.created
);
setRecentThreads(sorted);
} catch (error) {
} finally {
setIsLoading(false)
// dispatch(setIsLoadingCustom(null));
}
},
[secretKey]
);
const getMessages = React.useCallback(async () => {
// if ( !groupId || members?.length === 0) return;
if (!groupId) return;
await getMailMessages(groupId, members);
}, [getMailMessages, groupId, members, secretKey]);
const interval = useRef<any>(null);
const firstMount = useRef(false);
const filterModeRef = useRef("");
useEffect(() => {
if (filterModeRef.current !== filterMode) {
firstMount.current = false;
}
// if (groupId && !firstMount.current && members.length > 0) {
if (groupId && !firstMount.current) {
if (filterMode === "Recently active") {
getMessages();
} else if (filterMode === "Newest") {
getAllThreads(groupId, "Newest", true);
} else if (filterMode === "Oldest") {
getAllThreads(groupId, "Oldest", true);
}
setTempData()
firstMount.current = true;
}
}, [groupId, members, filterMode]);
const closeThread = useCallback(() => {
setCurrentThread(null);
}, []);
const getGroupMembers = useCallback(async (groupNumber: string) => {
try {
const response = await fetch(`/groups/members/${groupNumber}?limit=0`);
const groupData = await response.json();
let members: any = {};
if (groupData && Array.isArray(groupData?.members)) {
for (const member of groupData.members) {
if (member.member) {
// const res = await getNameInfo(member.member);
// const resAddress = await qortalRequest({
// action: "GET_ACCOUNT_DATA",
// address: member.member,
// });
const name = res;
const publicKey = resAddress.publicKey;
if (name) {
members[name] = {
publicKey,
address: member.member,
};
}
}
}
}
setMembers(members);
} catch (error) {
console.log({ error });
}
}, []);
// useEffect(() => {
// if(groupId){
// getGroupMembers(groupId);
// interval.current = setInterval(async () => {
// getGroupMembers(groupId);
// }, 180000)
// }
// return () => {
// if (interval?.current) {
// clearInterval(interval.current)
// }
// }
// }, [getGroupMembers, groupId]);
let listOfThreadsToDisplay = recentThreads;
if (filterMode === "Newest" || filterMode === "Oldest") {
listOfThreadsToDisplay = allThreads;
}
const onSubmitNewThread = useCallback(
(val: any) => {
if (filterMode === "Recently active") {
setRecentThreads((prev) => [val, ...prev]);
} else if (filterMode === "Newest") {
setAllThreads((prev) => [val, ...prev]);
}
},
[filterMode]
);
// useEffect(()=> {
// if(user?.name){
// const threads = JSON.parse(
// localStorage.getItem(`qmail_threads_viewedtimestamp_${user.name}`) || "{}"
// );
// setViewedThreads(threads)
// }
// }, [user?.name, currentThread])
const handleCloseThreadFilterList = () => {
setIsOpenFilterList(false);
};
const refetchThreadsLists = useCallback(()=> {
if (filterMode === "Recently active") {
getMessages();
} else if (filterMode === "Newest") {
getAllThreads(groupId, "Newest", true);
} else if (filterMode === "Oldest") {
getAllThreads(groupId, "Oldest", true);
}
}, [filterMode])
const updateThreadActivityCurrentThread = ()=> {
if(!currentThread) return
const thread = currentThread
updateThreadActivity({
threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread
})
}
const setThreadFunc = (data)=> {
const thread = data
setCurrentThread(thread);
if(thread?.threadId && thread?.threadData?.name){
updateThreadActivity({
threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread
})
}
setTimeout(() => {
executeEvent("threadFetchMode", {
mode: "last-page"
});
}, 300);
}
useEffect(()=> {
if(defaultThread){
setThreadFunc(defaultThread)
setDefaultThread(null)
}
}, [defaultThread])
const combinedListTempAndReal = useMemo(() => {
// Combine the two lists
const transformTempPublishedList = tempPublishedList.map((item)=> {
return {
...item,
threadData: item.tempData,
threadOwner: item?.name,
threadId: item.identifier
}
})
const combined = [...transformTempPublishedList, ...listOfThreadsToDisplay];
// Remove duplicates based on the "identifier"
const uniqueItems = new Map();
combined.forEach(item => {
uniqueItems.set(item.threadId, item); // This will overwrite duplicates, keeping the last occurrence
});
// Convert the map back to an array and sort by "created" timestamp in descending order
const sortedList = Array.from(uniqueItems.values()).sort((a, b) => b.threadData?.createdAt - a.threadData?.createdAt);
return sortedList;
}, [tempPublishedList, listOfThreadsToDisplay]);
if (currentThread)
return (
<Thread
currentThread={currentThread}
groupInfo={selectedGroup}
closeThread={closeThread}
members={members}
userInfo={userInfo}
secretKey={secretKey}
getSecretKey={getSecretKey}
updateThreadActivityCurrentThread={updateThreadActivityCurrentThread}
/>
);
return (
<GroupContainer
sx={{
position: "relative",
overflow: "auto",
width: "100%",
}}
>
<Popover
open={isOpenFilterList}
anchorEl={anchorElInstanceFilter.current}
onClose={handleCloseThreadFilterList}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
>
<InstanceListParent
sx={{
minHeight: "unset",
width: "auto",
padding: "0px",
}}
>
<InstanceListHeader></InstanceListHeader>
<InstanceListContainer>
{filterOptions?.map((filter) => {
return (
<InstanceListContainerRow
onClick={() => {
setFilterMode(filter);
}}
sx={{
backgroundColor:
filterMode === filter ? "rgba(74, 158, 244, 1)" : "unset",
}}
key={filter}
>
<InstanceListContainerRowCheck>
{filter === filterMode && (
<InstanceListContainerRowCheckIcon src={CheckSVG} />
)}
</InstanceListContainerRowCheck>
<InstanceListContainerRowMain>
<InstanceListContainerRowMainP>
{filter}
</InstanceListContainerRowMainP>
</InstanceListContainerRowMain>
</InstanceListContainerRow>
);
})}
</InstanceListContainer>
<InstanceFooter></InstanceFooter>
</InstanceListParent>
</Popover>
<ThreadContainerFullWidth>
<ThreadContainer>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<NewThread
groupInfo={selectedGroup}
refreshLatestThreads={getMessages}
members={members}
publishCallback={setTempData}
userInfo={userInfo}
getSecretKey={getSecretKey}
myName={userInfo?.name}
/>
<ComposeContainerBlank
sx={{
height: "auto",
}}
>
{selectedGroup && !currentThread && (
<ComposeContainer
onClick={() => {
setIsOpenFilterList(true);
}}
ref={anchorElInstanceFilter}
>
<ComposeIcon src={SortSVG} />
<SelectInstanceContainerFilterInner>
<ComposeP>Sort by</ComposeP>
<ArrowDownIcon src={ArrowDownSVG} />
</SelectInstanceContainerFilterInner>
</ComposeContainer>
)}
</ComposeContainerBlank>
</Box>
<Spacer height="30px" />
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<AllThreadP>{filterMode}</AllThreadP>
<RefreshIcon onClick={refetchThreadsLists} sx={{
color: 'white',
cursor: 'pointer'
}} />
</Box>
<Spacer height="30px" />
{combinedListTempAndReal.map((thread) => {
const hasViewedRecent =
viewedThreads[
`qmail_threads_${thread?.threadData?.groupId}_${thread?.threadId}`
];
const shouldAppearLighter =
hasViewedRecent &&
filterMode === "Recently active" &&
thread?.threadData?.createdAt < hasViewedRecent?.timestamp;
return (
<SingleThreadParent
onClick={() => {
setCurrentThread(thread);
if(thread?.threadId && thread?.threadData?.name){
updateThreadActivity({
threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread
})
}
}}
>
<Avatar
sx={{
height: "50px",
width: "50px",
}}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${thread?.threadData?.name}/qortal_avatar?async=true`}
alt={thread?.threadData?.name}
>
{thread?.threadData?.name?.charAt(0)}
</Avatar>
<ThreadInfoColumn>
<ThreadInfoColumnNameP>
<ThreadInfoColumnbyP>by </ThreadInfoColumnbyP>
{thread?.threadData?.name}
</ThreadInfoColumnNameP>
<ThreadInfoColumnTime>
{formatTimestamp(thread?.threadData?.createdAt)}
</ThreadInfoColumnTime>
</ThreadInfoColumn>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
}}
>
<ThreadSingleTitle
sx={{
fontWeight: shouldAppearLighter && 300,
}}
>
{thread?.threadData?.title}
</ThreadSingleTitle>
{filterMode === "Recently active" && (
<div
style={{
display: "flex",
alignItems: "center",
}}
>
<ThreadSingleLastMessageP>
<ThreadSingleLastMessageSpanP>
last message:{" "}
</ThreadSingleLastMessageSpanP>
{formatDate(thread?.created)}
</ThreadSingleLastMessageP>
</div>
)}
</div>
<Box onClick={()=> {
setTimeout(() => {
executeEvent("threadFetchMode", {
mode: "last-page"
});
}, 300);
}} sx={{
position: 'absolute',
bottom: '2px',
right: '2px',
borderRadius: '5px',
backgroundColor: '#27282c',
display: 'flex',
gap: '10px',
alignItems: 'center',
padding: '5px',
cursor: 'pointer',
'&:hover': {
background: 'rgba(255, 255, 255, 0.60)'
}
}}>
<Typography sx={{
color: 'white',
fontSize: '12px'
}}>Last page</Typography>
<ArrowForwardIosIcon sx={{
color: 'white',
fontSize: '12px'
}} />
</Box>
</SingleThreadParent>
);
})}
<Box
sx={{
width: "100%",
justifyContent: "center",
}}
>
{listOfThreadsToDisplay.length >= 20 &&
filterMode !== "Recently active" && (
<LazyLoad
onLoadMore={() => getAllThreads(groupId, filterMode, false)}
></LazyLoad>
)}
</Box>
</ThreadContainer>
</ThreadContainerFullWidth>
<LoadingSnackbar
open={isLoading}
info={{
message: "Loading threads... please wait.",
}}
/>
</GroupContainer>
);
};

View File

@@ -0,0 +1,799 @@
import {
AppBar,
Button,
Toolbar,
Typography,
Box,
TextField,
} from "@mui/material";
import { styled } from "@mui/system";
export const InstanceContainer = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
width: "100%",
backgroundColor: "var(--color-instance)",
height: "59px",
flexShrink: 0,
justifyContent: "space-between",
}));
export const MailContainer = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
width: "100%",
height: "calc(100vh - 78px)",
overflow: "hidden",
}));
export const MailBody = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "row",
width: "100%",
height: "calc(100% - 59px)",
// overflow: 'auto !important'
}));
export const MailBodyInner = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
width: "50%",
height: "100%",
}));
export const MailBodyInnerHeader = styled(Box)(({ theme }) => ({
display: "flex",
width: "100%",
height: "25px",
marginTop: "50px",
marginBottom: "35px",
justifyContent: "center",
alignItems: "center",
gap: "11px",
}));
export const MailBodyInnerScroll = styled(Box)`
display: flex;
flex-direction: column;
overflow: auto !important;
transition: background-color 0.3s;
height: calc(100% - 110px);
&::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: transparent; /* Initially transparent */
transition: background-color 0.3s; /* Transition for background color */
}
&::-webkit-scrollbar-thumb {
background-color: transparent; /* Initially transparent */
border-radius: 3px; /* Scrollbar thumb radius */
transition: background-color 0.3s; /* Transition for thumb color */
}
&:hover {
&::-webkit-scrollbar {
background-color: #494747; /* Scrollbar background color on hover */
}
&::-webkit-scrollbar-thumb {
background-color: #ffffff3d; /* Scrollbar thumb color on hover */
}
&::-webkit-scrollbar-thumb:hover {
background-color: #ffffff3d; /* Color when hovering over the thumb */
}
}
`;
export const ComposeContainer = styled(Box)(({ theme }) => ({
display: "flex",
width: "150px",
alignItems: "center",
gap: "7px",
height: "100%",
cursor: "pointer",
transition: "0.2s background-color",
justifyContent: "center",
"&:hover": {
backgroundColor: "rgba(67, 68, 72, 1)",
},
}));
export const ComposeContainerBlank = styled(Box)(({ theme }) => ({
display: "flex",
width: "150px",
alignItems: "center",
gap: "7px",
height: "100%",
}));
export const ComposeP = styled(Typography)(({ theme }) => ({
fontSize: "15px",
fontWeight: 500,
}));
export const ComposeIcon = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
cursor: "pointer",
});
export const ArrowDownIcon = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
cursor: "pointer",
});
export const MailIconImg = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
});
export const MailMessageRowInfoImg = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
});
export const SelectInstanceContainer = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: "17px",
}));
export const SelectInstanceContainerInner = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: "3px",
cursor: "pointer",
padding: "8px",
transition: "all 0.2s",
"&:hover": {
borderRadius: "8px",
background: "#434448",
},
}));
export const SelectInstanceContainerFilterInner = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: "3px",
cursor: "pointer",
padding: "8px",
transition: "all 0.2s"
}));
export const InstanceLabel = styled(Typography)(({ theme }) => ({
fontSize: "16px",
fontWeight: 500,
color: "#FFFFFF33",
}));
export const InstanceP = styled(Typography)(({ theme }) => ({
fontSize: "16px",
fontWeight: 500,
}));
export const MailMessageRowContainer = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
cursor: "pointer",
justifyContent: "space-between",
borderRadius: "56px 5px 10px 56px",
paddingRight: "15px",
transition: "background 0.2s",
gap: "10px",
"&:hover": {
background: "#434448",
},
}));
export const MailMessageRowProfile = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
cursor: "pointer",
justifyContent: "flex-start",
gap: "10px",
width: "50%",
overflow: "hidden",
}));
export const MailMessageRowInfo = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
cursor: "pointer",
justifyContent: "flex-start",
gap: "7px",
width: "50%",
}));
export const MailMessageRowInfoStatusNotDecrypted = styled(Typography)(
({ theme }) => ({
fontSize: "16px",
fontWeight: 900,
textTransform: "uppercase",
paddingTop: "2px",
})
);
export const MailMessageRowInfoStatusRead = styled(Typography)(({ theme }) => ({
fontSize: "16px",
fontWeight: 300,
}));
export const MessageExtraInfo = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
gap: "2px",
overflow: "hidden",
}));
export const MessageExtraName = styled(Typography)(({ theme }) => ({
fontSize: "16px",
fontWeight: 900,
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}));
export const MessageExtraDate = styled(Typography)(({ theme }) => ({
fontSize: "15px",
fontWeight: 500,
}));
export const MessagesContainer = styled(Box)(({ theme }) => ({
width: "460px",
maxWidth: "90%",
display: "flex",
flexDirection: "column",
gap: "12px",
}));
export const InstanceListParent = styled(Box)`
display: flex;
flex-direction: column;
width: 100%;
min-height: 246px;
max-height: 325px;
width: 425px;
padding: 10px 0px 7px 0px;
background-color: var(--color-instance-popover-bg);
border: 1px solid rgba(0, 0, 0, 0.1);
`;
export const InstanceListHeader = styled(Box)`
display: flex;
flex-direction: column;
width: 100%;
background-color: var(--color-instance-popover-bg);
`;
export const InstanceFooter = styled(Box)`
display: flex;
flex-direction: column;
width: 100%;
flex-shrink: 0;
`;
export const InstanceListContainer = styled(Box)`
width: 100%;
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: auto !important;
transition: background-color 0.3s;
&::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: transparent; /* Initially transparent */
transition: background-color 0.3s; /* Transition for background color */
}
&::-webkit-scrollbar-thumb {
background-color: transparent; /* Initially transparent */
border-radius: 3px; /* Scrollbar thumb radius */
transition: background-color 0.3s; /* Transition for thumb color */
}
&:hover {
&::-webkit-scrollbar {
background-color: #494747; /* Scrollbar background color on hover */
}
&::-webkit-scrollbar-thumb {
background-color: #ffffff3d; /* Scrollbar thumb color on hover */
}
&::-webkit-scrollbar-thumb:hover {
background-color: #ffffff3d; /* Color when hovering over the thumb */
}
}
`;
export const InstanceListContainerRowLabelContainer = styled(Box)(
({ theme }) => ({
width: "100%",
display: "flex",
alignItems: "center",
gap: "10px",
height: "50px",
})
);
export const InstanceListContainerRow = styled(Box)(({ theme }) => ({
width: "100%",
display: "flex",
alignItems: "center",
gap: "10px",
height: "50px",
cursor: "pointer",
transition: "0.2s background",
"&:hover": {
background: "rgba(67, 68, 72, 1)",
},
flexShrink: 0,
}));
export const InstanceListContainerRowCheck = styled(Box)(({ theme }) => ({
width: "47px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}));
export const InstanceListContainerRowMain = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: "space-between",
width: "100%",
alignItems: "center",
paddingRight: "30px",
overflow: "hidden",
}));
export const CloseParent = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: "20px",
}));
export const InstanceListContainerRowMainP = styled(Typography)(
({ theme }) => ({
fontWeight: 500,
fontSize: "16px",
textOverflow: "ellipsis",
overflow: "hidden",
})
);
export const InstanceListContainerRowCheckIcon = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
});
export const InstanceListContainerRowGroupIcon = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
});
export const TypeInAliasTextfield = styled(TextField)({
width: "340px", // Adjust the width as needed
borderRadius: "5px",
backgroundColor: "rgba(30, 30, 32, 1)",
border: "none",
outline: "none",
input: {
fontSize: 16,
color: "white",
"&::placeholder": {
fontSize: 16,
color: "rgba(255, 255, 255, 0.2)",
},
border: "none",
outline: "none",
padding: "10px",
},
"& .MuiOutlinedInput-root": {
"& fieldset": {
border: "none",
},
"&:hover fieldset": {
border: "none",
},
"&.Mui-focused fieldset": {
border: "none",
},
},
"& .MuiInput-underline:before": {
borderBottom: "none",
},
"& .MuiInput-underline:hover:not(.Mui-disabled):before": {
borderBottom: "none",
},
"& .MuiInput-underline:after": {
borderBottom: "none",
},
});
export const NewMessageCloseImg = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
cursor: "pointer",
});
export const NewMessageHeaderP = styled(Typography)(({ theme }) => ({
fontSize: "18px",
fontWeight: 600,
}));
export const NewMessageInputRow = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
borderBottom: "3px solid rgba(237, 239, 241, 1)",
width: "100%",
paddingBottom: "6px",
}));
export const NewMessageInputLabelP = styled(Typography)`
color: rgba(84, 84, 84, 0.7);
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: 120%; /* 24px */
letter-spacing: 0.15px;
`;
export const AliasLabelP = styled(Typography)`
color: rgba(84, 84, 84, 0.7);
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 120%; /* 24px */
letter-spacing: 0.15px;
transition: color 0.2s;
cursor: pointer;
&:hover {
color: rgba(43, 43, 43, 1);
}
`;
export const NewMessageAliasContainer = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
gap: "12px",
}));
export const AttachmentContainer = styled(Box)(({ theme }) => ({
height: "36px",
width: "100%",
display: "flex",
alignItems: "center",
}));
export const NewMessageAttachmentImg = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
cursor: "pointer",
padding: "10px",
border: "1px dashed #646464",
});
export const NewMessageSendButton = styled(Box)`
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.9);
display: inline-flex;
padding: 8px 16px 8px 12px;
justify-content: center;
align-items: center;
gap: 8px;
width: fit-content;
transition: all 0.2s;
color: black;
min-width: 120px;
gap: 8px;
position: relative;
cursor: pointer;
&:hover {
background-color: rgba(41, 41, 43, 1);
color: white;
svg path {
fill: white; // Fill color changes to white on hover
}
}
`;
export const NewMessageSendP = styled(Typography)`
font-family: Roboto;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 120%; /* 19.2px */
letter-spacing: -0.16px;
`;
export const ShowMessageNameP = styled(Typography)`
font-family: Roboto;
font-size: 16px;
font-weight: 900;
line-height: 19px;
letter-spacing: 0em;
text-align: left;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`;
export const ShowMessageTimeP = styled(Typography)`
color: rgba(255, 255, 255, 0.5);
font-family: Roboto;
font-size: 15px;
font-style: normal;
font-weight: 500;
line-height: normal;
`;
export const ShowMessageSubjectP = styled(Typography)`
font-family: Roboto;
font-size: 16px;
font-weight: 500;
line-height: 19px;
letter-spacing: 0.0075em;
text-align: left;
`;
export const ShowMessageButton = styled(Box)`
display: inline-flex;
padding: 8px 16px 8px 16px;
align-items: center;
justify-content: center;
gap: 8px;
width: fit-content;
transition: all 0.2s;
color: white;
background-color: rgba(41, 41, 43, 1)
min-width: 120px;
gap: 8px;
border-radius: 4px;
border: 0.5px solid rgba(255, 255, 255, 0.70);
font-family: Roboto;
min-width: 120px;
cursor: pointer;
&:hover {
border-radius: 4px;
border: 0.5px solid rgba(255, 255, 255, 0.70);
background: #434448;
}
`;
export const ShowMessageReturnButton = styled(Box)`
display: inline-flex;
padding: 8px 16px 8px 16px;
align-items: center;
justify-content: center;
gap: 8px;
width: fit-content;
transition: all 0.2s;
color: white;
background-color: rgba(41, 41, 43, 1)
min-width: 120px;
gap: 8px;
border-radius: 4px;
font-family: Roboto;
min-width: 120px;
cursor: pointer;
&:hover {
border-radius: 4px;
background: #434448;
}
`;
export const ShowMessageButtonP = styled(Typography)`
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 120%; /* 19.2px */
letter-spacing: -0.16px;
color: white;
`;
export const ShowMessageButtonImg = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
cursor: "pointer",
});
export const MailAttachmentImg = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
});
export const AliasAvatarImg = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
});
export const MoreImg = styled("img")({
width: "auto",
height: "auto",
userSelect: "none",
objectFit: "contain",
transition: "0.2s all",
"&:hover": {
transform: "scale(1.3)",
},
});
export const MoreP = styled(Typography)`
color: rgba(255, 255, 255, 0.5);
/* Attachments */
font-family: Roboto;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 120%; /* 19.2px */
letter-spacing: -0.16px;
white-space: nowrap;
`;
export const ThreadContainerFullWidth = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
width: "100%",
alignItems: "center",
}));
export const ThreadContainer = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
width: "100%",
maxWidth: "95%",
}));
export const GroupNameP = styled(Typography)`
color: #fff;
font-size: 25px;
font-style: normal;
font-weight: 700;
line-height: 120%; /* 30px */
letter-spacing: 0.188px;
`;
export const AllThreadP = styled(Typography)`
color: #FFF;
font-size: 20px;
font-style: normal;
font-weight: 400;
line-height: 120%; /* 24px */
letter-spacing: 0.15px;
`;
export const SingleThreadParent = styled(Box)`
border-radius: 35px 4px 4px 35px;
position: relative;
background: #434448;
display: flex;
padding: 13px;
cursor: pointer;
margin-bottom: 5px;
height: 76px;
align-items:center;
transition: 0.2s all;
&:hover {
background: rgba(255, 255, 255, 0.20)
}
`;
export const SingleTheadMessageParent = styled(Box)`
border-radius: 35px 4px 4px 35px;
background: #434448;
display: flex;
padding: 13px;
cursor: pointer;
margin-bottom: 5px;
height: 76px;
align-items:center;
`;
export const ThreadInfoColumn = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
width: "170px",
gap: '2px',
marginLeft: '10px',
height: '100%',
justifyContent: 'center'
}));
export const ThreadInfoColumnNameP = styled(Typography)`
color: #FFF;
font-family: Roboto;
font-size: 16px;
font-style: normal;
font-weight: 900;
line-height: normal;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`;
export const ThreadInfoColumnbyP = styled('span')`
color: rgba(255, 255, 255, 0.80);
font-family: Roboto;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: normal;
`;
export const ThreadInfoColumnTime = styled(Typography)`
color: rgba(255, 255, 255, 0.80);
font-family: Roboto;
font-size: 15px;
font-style: normal;
font-weight: 500;
line-height: normal;
`
export const ThreadSingleTitle = styled(Typography)`
color: #FFF;
font-family: Roboto;
font-size: 23px;
font-style: normal;
font-weight: 700;
line-height: normal;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`
export const ThreadSingleLastMessageP = styled(Typography)`
color: #FFF;
font-family: Roboto;
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: normal;
`
export const ThreadSingleLastMessageSpanP = styled('span')`
color: #FFF;
font-family: Roboto;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
`;
export const GroupContainer = styled(Box)`
position: relative;
overflow: auto;
width: 100%;
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-track:hover {
background-color: transparent;
}
&::-webkit-scrollbar {
width: 16px;
height: 10px;
background-color: white;
}
&::-webkit-scrollbar-thumb {
background-color: #838eee;
border-radius: 8px;
background-clip: content-box;
border: 4px solid transparent;
}
&::-webkit-scrollbar-thumb:hover {
background-color: #6270f0;
}
`
export const CloseContainer = styled(Box)(({ theme }) => ({
display: "flex",
width: "50px",
overflow: "hidden",
alignItems: "center",
cursor: "pointer",
transition: "0.2s background-color",
justifyContent: "center",
position: 'absolute',
top: '0px',
right: '0px',
height: '50px',
borderRadius: '0px 12px 0px 0px',
"&:hover": {
backgroundColor: "rgba(162, 31, 31, 1)",
},
}));

View File

@@ -0,0 +1,554 @@
import React, { useEffect, useRef, useState } from "react";
import { Box, Button, CircularProgress, Input, Typography } from "@mui/material";
import ShortUniqueId from "short-unique-id";
import CloseIcon from "@mui/icons-material/Close";
import ModalCloseSVG from "../../../assets/svgs/ModalClose.svg";
import ComposeIconSVG from "../../../assets/svgs/ComposeIcon.svg";
import {
AttachmentContainer,
CloseContainer,
ComposeContainer,
ComposeIcon,
ComposeP,
InstanceFooter,
InstanceListContainer,
InstanceListHeader,
NewMessageAttachmentImg,
NewMessageCloseImg,
NewMessageHeaderP,
NewMessageInputRow,
NewMessageSendButton,
NewMessageSendP,
} from "./Mail-styles";
import { ReusableModal } from "./ReusableModal";
import { Spacer } from "../../../common/Spacer";
import { formatBytes } from "../../../utils/Size";
import { CreateThreadIcon } from "../../../assets/svgs/CreateThreadIcon";
import { SendNewMessage } from "../../../assets/svgs/SendNewMessage";
import { TextEditor } from "./TextEditor";
import { MyContext, pauseAllQueues, resumeAllQueues } from "../../../App";
import { getFee } from "../../../background";
import TipTap from "../../Chat/TipTap";
import { MessageDisplay } from "../../Chat/MessageDisplay";
import { CustomizedSnackbars } from "../../Snackbar/Snackbar";
import { saveTempPublish } from "../../Chat/GroupAnnouncements";
const uid = new ShortUniqueId({ length: 8 });
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => {
reject(error);
};
});
export function objectToBase64(obj: any) {
// Step 1: Convert the object to a JSON string
const jsonString = JSON.stringify(obj);
// Step 2: Create a Blob from the JSON string
const blob = new Blob([jsonString], { type: "application/json" });
// Step 3: Create a FileReader to read the Blob as a base64-encoded string
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (typeof reader.result === "string") {
// Remove 'data:application/json;base64,' prefix
const base64 = reader.result.replace(
"data:application/json;base64,",
""
);
resolve(base64);
} else {
reject(new Error("Failed to read the Blob as a base64-encoded string"));
}
};
reader.onerror = () => {
reject(reader.error);
};
reader.readAsDataURL(blob);
});
}
interface NewMessageProps {
hideButton?: boolean;
groupInfo: any;
currentThread?: any;
isMessage?: boolean;
messageCallback?: (val: any) => void;
publishCallback?: () => void;
refreshLatestThreads?: () => void;
members: any;
}
export const publishGroupEncryptedResource = async ({
encryptedData,
identifier,
}) => {
return new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "publishGroupEncryptedResource",
payload: {
encryptedData,
identifier,
},
},
(response) => {
if (!response?.error) {
res(response);
return
}
rej(response.error);
}
);
});
};
export const encryptSingleFunc = async (data: string, secretKeyObject: any) => {
try {
return new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "encryptSingle",
payload: {
data,
secretKeyObject,
},
},
(response) => {
if (!response?.error) {
res(response);
return;
}
rej(response.error);
}
);
});
} catch (error) {}
};
export const NewThread = ({
groupInfo,
members,
currentThread,
isMessage = false,
publishCallback,
userInfo,
getSecretKey,
closeCallback,
postReply,
myName
}: NewMessageProps) => {
const { show } = React.useContext(MyContext);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [value, setValue] = useState("");
const [isSending, setIsSending] = useState(false);
const [threadTitle, setThreadTitle] = useState<string>("");
const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null);
const editorRef = useRef(null);
const setEditorRef = (editorInstance) => {
editorRef.current = editorInstance;
};
useEffect(() => {
if (postReply) {
setIsOpen(true);
}
}, [postReply]);
const closeModal = () => {
setIsOpen(false);
setValue("");
};
async function publishQDNResource() {
try {
pauseAllQueues()
if(isSending) return
setIsSending(true)
let name: string = "";
let errorMsg = "";
name = userInfo?.name || "";
const missingFields: string[] = [];
if (!isMessage && !threadTitle) {
errorMsg = "Please provide a thread title";
}
if (!name) {
errorMsg = "Cannot send a message without a access to your name";
}
if (!groupInfo) {
errorMsg = "Cannot access group information";
}
// if (!description) missingFields.push('subject')
if (missingFields.length > 0) {
const missingFieldsString = missingFields.join(", ");
const errMsg = `Missing: ${missingFieldsString}`;
errorMsg = errMsg;
}
if (errorMsg) {
// dispatch(
// setNotification({
// msg: errorMsg,
// alertType: "error",
// })
// );
throw new Error(errorMsg);
}
const htmlContent = editorRef.current.getHTML();
if (!htmlContent?.trim() || htmlContent?.trim() === "<p></p>")
throw new Error("Please provide a first message to the thread");
const fee = await getFee("ARBITRARY");
let feeToShow = fee.fee;
if (!isMessage) {
feeToShow = +feeToShow * 2;
}
await show({
message: "Would you like to perform a ARBITRARY transaction?",
publishFee: feeToShow + " QORT",
});
let reply = null;
if (postReply) {
reply = { ...postReply };
if (reply.reply) {
delete reply.reply;
}
}
const mailObject: any = {
createdAt: Date.now(),
version: 1,
textContentV2: htmlContent,
name,
threadOwner: currentThread?.threadData?.name || name,
reply,
};
const secretKey = await getSecretKey();
if (!secretKey) {
throw new Error("Cannot get group secret key");
}
if (!isMessage) {
const idThread = uid.rnd();
const idMsg = uid.rnd();
const messageToBase64 = await objectToBase64(mailObject);
const encryptSingleFirstPost = await encryptSingleFunc(
messageToBase64,
secretKey
);
const threadObject = {
title: threadTitle,
groupId: groupInfo.id,
createdAt: Date.now(),
name,
};
const threadToBase64 = await objectToBase64(threadObject);
const encryptSingleThread = await encryptSingleFunc(
threadToBase64,
secretKey
);
let identifierThread = `grp-${groupInfo.groupId}-thread-${idThread}`;
await publishGroupEncryptedResource({
identifier: identifierThread,
encryptedData: encryptSingleThread,
});
let identifierPost = `thmsg-${identifierThread}-${idMsg}`;
await publishGroupEncryptedResource({
identifier: identifierPost,
encryptedData: encryptSingleFirstPost,
});
const dataToSaveToStorage = {
name: myName,
identifier: identifierThread,
service: 'DOCUMENT',
tempData: threadObject,
created: Date.now(),
}
const dataToSaveToStoragePost = {
name: myName,
identifier: identifierPost,
service: 'DOCUMENT',
tempData: mailObject,
created: Date.now(),
threadId: identifierThread
}
await saveTempPublish({data: dataToSaveToStorage, key: 'thread'})
await saveTempPublish({data: dataToSaveToStoragePost, key: 'thread-post'})
setInfoSnack({
type: "success",
message: "Successfully created thread. It may take some time for the publish to propagate",
});
setOpenSnack(true)
// dispatch(
// setNotification({
// msg: "Message sent",
// alertType: "success",
// })
// );
if (publishCallback) {
publishCallback()
// threadCallback({
// threadData: threadObject,
// threadOwner: name,
// name,
// threadId: identifierThread,
// created: Date.now(),
// service: 'MAIL_PRIVATE',
// identifier: identifier
// })
}
closeModal();
} else {
if (!currentThread) throw new Error("unable to locate thread Id");
const idThread = currentThread.threadId;
const messageToBase64 = await objectToBase64(mailObject);
const encryptSinglePost = await encryptSingleFunc(
messageToBase64,
secretKey
);
const idMsg = uid.rnd();
let identifier = `thmsg-${idThread}-${idMsg}`;
const res = await publishGroupEncryptedResource({
identifier: identifier,
encryptedData: encryptSinglePost,
});
const dataToSaveToStoragePost = {
threadId: idThread,
name: myName,
identifier: identifier,
service: 'DOCUMENT',
tempData: mailObject,
created: Date.now()
}
await saveTempPublish({data: dataToSaveToStoragePost, key: 'thread-post'})
// await qortalRequest(multiplePublishMsg);
// dispatch(
// setNotification({
// msg: "Message sent",
// alertType: "success",
// })
// );
setInfoSnack({
type: "success",
message: "Successfully created post. It may take some time for the publish to propagate",
});
setOpenSnack(true)
if(publishCallback){
publishCallback()
}
// messageCallback({
// identifier,
// id: identifier,
// name,
// service: MAIL_SERVICE_TYPE,
// created: Date.now(),
// ...mailObject,
// });
}
closeModal();
} catch (error: any) {
if(error?.message){
setInfoSnack({
type: "error",
message: error?.message,
});
setOpenSnack(true)
}
} finally {
setIsSending(false);
resumeAllQueues()
}
}
const sendMail = () => {
publishQDNResource();
};
return (
<Box
sx={{
display: "flex",
}}
>
<ComposeContainer
sx={{
padding: "15px",
}}
onClick={() => setIsOpen(true)}
>
<ComposeIcon src={ComposeIconSVG} />
<ComposeP>{currentThread ? "New Post" : "New Thread"}</ComposeP>
</ComposeContainer>
<ReusableModal
open={isOpen}
customStyles={{
maxHeight: "95vh",
maxWidth: "950px",
height: "700px",
borderRadius: "12px 12px 0px 0px",
background: "#434448",
padding: "0px",
gap: "0px",
}}
>
<InstanceListHeader
sx={{
height: "50px",
padding: "20px 42px",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: "#434448",
}}
>
<NewMessageHeaderP>
{isMessage ? "Post Message" : "New Thread"}
</NewMessageHeaderP>
<CloseContainer onClick={closeModal}>
<NewMessageCloseImg src={ModalCloseSVG} />
</CloseContainer>
</InstanceListHeader>
<InstanceListContainer
sx={{
backgroundColor: "#434448",
padding: "20px 42px",
height: "calc(100% - 150px)",
flexShrink: 0,
}}
>
{!isMessage && (
<>
<Spacer height="10px" />
<NewMessageInputRow>
<Input
id="standard-adornment-name"
value={threadTitle}
onChange={(e) => {
setThreadTitle(e.target.value);
}}
placeholder="Thread Title"
disableUnderline
autoComplete="off"
autoCorrect="off"
sx={{
width: "100%",
color: "white",
"& .MuiInput-input::placeholder": {
color: "rgba(255,255,255, 0.70) !important",
fontSize: "20px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "120%", // 24px
letterSpacing: "0.15px",
opacity: 1,
},
"&:focus": {
outline: "none",
},
// Add any additional styles for the input here
}}
/>
</NewMessageInputRow>
</>
)}
{postReply && postReply.textContentV2 && (
<Box
sx={{
width: "100%",
maxHeight: "120px",
overflow: "auto",
}}
>
<MessageDisplay htmlContent={postReply?.textContentV2} />
</Box>
)}
<Spacer height="30px" />
<Box
sx={{
maxHeight: "40vh",
}}
>
<TipTap
setEditorRef={setEditorRef}
onEnter={sendMail}
disableEnter
/>
{/* <TextEditor
inlineContent={value}
setInlineContent={(val: any) => {
setValue(val);
}}
/> */}
</Box>
</InstanceListContainer>
<InstanceFooter
sx={{
backgroundColor: "#434448",
padding: "20px 42px",
alignItems: "center",
height: "90px",
}}
>
<NewMessageSendButton onClick={sendMail}>
{isSending && (
<Box sx={{height: '100%', position: 'absolute', width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
<CircularProgress sx={{
}} size={'12px'} />
</Box>
)}
<NewMessageSendP>
{isMessage ? "Post" : "Create Thread"}
</NewMessageSendP>
{isMessage ? (
<SendNewMessage
color="red"
opacity={1}
height="25px"
width="25px"
/>
) : (
<CreateThreadIcon
color="red"
opacity={1}
height="25px"
width="25px"
/>
)}
</NewMessageSendButton>
</InstanceFooter>
</ReusableModal>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
</Box>
);
};

View File

@@ -0,0 +1,129 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { createEditor} from 'slate';
import { withReact, Slate, Editable, RenderElementProps, RenderLeafProps } from 'slate-react';
type ExtendedRenderElementProps = RenderElementProps & { mode?: string }
export const renderElement = ({
attributes,
children,
element,
mode
}: ExtendedRenderElementProps) => {
switch (element.type) {
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
case 'heading-2':
return (
<h2
className="h2"
{...attributes}
style={{ textAlign: element.textAlign }}
>
{children}
</h2>
)
case 'heading-3':
return (
<h3
className="h3"
{...attributes}
style={{ textAlign: element.textAlign }}
>
{children}
</h3>
)
case 'code-block':
return (
<pre {...attributes} className="code-block">
<code>{children}</code>
</pre>
)
case 'code-line':
return <div {...attributes}>{children}</div>
case 'link':
return (
<a href={element.url} {...attributes}>
{children}
</a>
)
default:
return (
<p
className={`paragraph${mode ? `-${mode}` : ''}`}
{...attributes}
style={{ textAlign: element.textAlign }}
>
{children}
</p>
)
}
}
export const renderLeaf = ({ attributes, children, leaf }: RenderLeafProps) => {
let el = children
if (leaf.bold) {
el = <strong>{el}</strong>
}
if (leaf.italic) {
el = <em>{el}</em>
}
if (leaf.underline) {
el = <u>{el}</u>
}
if (leaf.link) {
el = (
<a href={leaf.link} {...attributes}>
{el}
</a>
)
}
return <span {...attributes}>{el}</span>
}
interface ReadOnlySlateProps {
content: any
mode?: string
}
const ReadOnlySlate: React.FC<ReadOnlySlateProps> = ({ content, mode }) => {
const [load, setLoad] = useState(false)
const editor = useMemo(() => withReact(createEditor()), [])
const value = useMemo(() => content, [content])
const performUpdate = useCallback(async()=> {
setLoad(true)
await new Promise<void>((res)=> {
setTimeout(() => {
res()
}, 250);
})
setLoad(false)
}, [])
useEffect(()=> {
performUpdate()
}, [value])
if(load) return null
return (
<Slate editor={editor} value={value} onChange={() => {}}>
<Editable
readOnly
renderElement={(props) => renderElement({ ...props, mode })}
renderLeaf={renderLeaf}
/>
</Slate>
)
}
export default ReadOnlySlate;

View File

@@ -0,0 +1,57 @@
import React from 'react'
import { Box, Modal, useTheme } from '@mui/material'
interface MyModalProps {
open: boolean
onClose?: () => void
onSubmit?: (obj: any) => Promise<void>
children: any
customStyles?: any
}
export const ReusableModal: React.FC<MyModalProps> = ({
open,
onClose,
onSubmit,
children,
customStyles = {}
}) => {
const theme = useTheme()
return (
<Modal
open={open}
onClose={onClose}
aria-labelledby="modal-title"
aria-describedby="modal-description"
componentsProps={{
backdrop: {
style: {
backdropFilter: 'blur(3px)',
},
},
}}
disableAutoFocus
disableEnforceFocus
disableRestoreFocus
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '75%',
bgcolor: theme.palette.primary.main,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
gap: 2,
...customStyles
}}
>
{children}
</Box>
</Modal>
)
}

View File

@@ -0,0 +1,224 @@
import React, { useState } from "react";
import { Avatar, Box, IconButton } from "@mui/material";
import DOMPurify from "dompurify";
import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
import MoreSVG from '../../../assets/svgs/More.svg'
import {
MoreImg,
MoreP,
SingleTheadMessageParent,
ThreadInfoColumn,
ThreadInfoColumnNameP,
ThreadInfoColumnTime,
} from "./Mail-styles";
import { Spacer } from "../../../common/Spacer";
import { DisplayHtml } from "./DisplayHtml";
import { formatTimestampForum } from "../../../utils/time";
import ReadOnlySlate from "./ReadOnlySlate";
import { MessageDisplay } from "../../Chat/MessageDisplay";
import { getBaseApi } from "../../../background";
import { getBaseApiReact } from "../../../App";
export const ShowMessage = ({ message, openNewPostWithQuote }: any) => {
const [expandAttachments, setExpandAttachments] = useState<boolean>(false);
let cleanHTML = "";
if (message?.htmlContent) {
cleanHTML = DOMPurify.sanitize(message.htmlContent);
}
return (
<SingleTheadMessageParent
sx={{
height: "auto",
alignItems: "flex-start",
cursor: "default",
borderRadius: '35px 4px 4px 4px'
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
width: '100%'
}}
>
<Box
sx={{
display: "flex",
alignItems: "flex-start",
gap: "10px",
}}
>
<Avatar sx={{
height: '50px',
width: '50px'
}} src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.name}/qortal_avatar?async=true`} alt={message?.name}>{message?.name?.charAt(0)}</Avatar>
<ThreadInfoColumn>
<ThreadInfoColumnNameP>{message?.name}</ThreadInfoColumnNameP>
<ThreadInfoColumnTime>
{formatTimestampForum(message?.created)}
</ThreadInfoColumnTime>
</ThreadInfoColumn>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
}}
>
{message?.attachments?.length > 0 && (
<Box
sx={{
width: "100%",
marginTop: "10px",
}}
>
{message?.attachments
.map((file: any, index: number) => {
const isFirst = index === 0
return (
<Box
sx={{
display: expandAttachments ? "flex" : !expandAttachments && isFirst ? 'flex' : 'none',
alignItems: "center",
justifyContent: "flex-start",
width: "100%",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
cursor: "pointer",
width: "auto",
}}
>
{/* <FileElement
fileInfo={{ ...file, mimeTypeSaved: file?.type }}
title={file?.filename}
mode="mail"
otherUser={message?.user}
>
<MailAttachmentImg src={AttachmentMailSVG} />
<Typography
sx={{
fontSize: "16px",
transition: '0.2s all',
"&:hover": {
color: 'rgba(255, 255, 255, 0.90)',
textDecoration: 'underline'
}
}}
>
{file?.originalFilename || file?.filename}
</Typography>
</FileElement> */}
{message?.attachments?.length > 1 && isFirst && (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
}}
onClick={() => {
setExpandAttachments(prev => !prev);
}}
>
<MoreImg
sx={{
marginLeft: "5px",
transform: expandAttachments
? "rotate(180deg)"
: "unset",
}}
src={MoreSVG}
/>
<MoreP>
{expandAttachments ? 'hide' : `(${message?.attachments?.length - 1} more)`}
</MoreP>
</Box>
)}
</Box>
</Box>
);
})
}
</Box>
)}
</div>
</Box>
<Spacer height="20px" />
{message?.reply?.textContentV2 && (
<>
<Box sx={{
width: '100%',
opacity: 0.7,
borderRadius: '5px',
border: '1px solid gray',
boxSizing: 'border-box',
padding: '5px'
}}>
<Box
sx={{
display: "flex",
alignItems: "flex-start",
gap: "10px",
}}
>
<Avatar sx={{
height: '30px',
width: '30px'
}} src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.reply?.name}/qortal_avatar?async=true`} alt={message?.reply?.name}>{message?.reply?.name?.charAt(0)}</Avatar>
<ThreadInfoColumn>
<ThreadInfoColumnNameP sx={{
fontSize: '14px'
}}>{message?.reply?.name}</ThreadInfoColumnNameP>
</ThreadInfoColumn>
</Box>
<MessageDisplay htmlContent={message?.reply?.textContentV2} />
</Box>
<Spacer height="20px" />
</>
)}
{message?.textContent && (
<ReadOnlySlate content={message.textContent} mode="mail" />
)}
{message?.textContentV2 && (
<MessageDisplay htmlContent={message?.textContentV2} />
)}
{message?.htmlContent && (
<div dangerouslySetInnerHTML={{ __html: cleanHTML }} />
)}
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'flex-end'
}}>
<IconButton
onClick={() => openNewPostWithQuote(message)}
>
<FormatQuoteIcon />
</IconButton>
</Box>
</Box>
</SingleTheadMessageParent>
);
};

View File

@@ -0,0 +1,39 @@
import React from "react";
import ReactQuill, { Quill } from "react-quill";
import "react-quill/dist/quill.snow.css";
import ImageResize from "quill-image-resize-module-react";
import './texteditor.css'
Quill.register("modules/imageResize", ImageResize);
const modules = {
imageResize: {
parchment: Quill.import("parchment"),
modules: ["Resize", "DisplaySize"],
},
toolbar: [
["bold", "italic", "underline", "strike"], // styled text
["blockquote", "code-block"], // blocks
[{ header: 1 }, { header: 2 }], // custom button values
[{ list: "ordered" }, { list: "bullet" }], // lists
[{ script: "sub" }, { script: "super" }], // superscript/subscript
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
[{ direction: "rtl" }], // text direction
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }], // custom button values
[{ color: [] }, { background: [] }], // dropdown with defaults
[{ font: [] }], // font family
[{ align: [] }], // text align
["clean"], // remove formatting
// ["image"], // image
],
};
export const TextEditor = ({ inlineContent, setInlineContent }: any) => {
return (
<ReactQuill
theme="snow"
value={inlineContent}
onChange={setInlineContent}
modules={modules}
/>
);
};

View File

@@ -0,0 +1,329 @@
import React, {
FC,
useCallback,
useEffect,
useRef,
useState
} from 'react'
import {
Box,
Skeleton,
} from '@mui/material'
import { ShowMessage } from './ShowMessageWithoutModal'
// import {
// setIsLoadingCustom,
// } from '../../state/features/globalSlice'
import { ComposeP, GroupContainer, GroupNameP, MailIconImg, ShowMessageReturnButton, SingleThreadParent, ThreadContainer, ThreadContainerFullWidth } from './Mail-styles'
import { Spacer } from '../../../common/Spacer'
import { threadIdentifier } from './GroupMail'
import LazyLoad from '../../../common/LazyLoad'
import ReturnSVG from '../../../assets/svgs/Return.svg'
import { NewThread } from './NewThread'
import { decryptPublishes } from '../../Chat/GroupAnnouncements'
import { getBaseApi } from '../../../background'
import { getBaseApiReact } from '../../../App'
interface ThreadProps {
currentThread: any
groupInfo: any
closeThread: () => void
members: any
}
const getEncryptedResource = async ({name, identifier, secretKey})=> {
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64`
);
const data = await res.text();
const response = await decryptPublishes([{ data }], secretKey);
const messageData = response[0];
return messageData.decryptedData
}
export const Thread = ({
currentThread,
groupInfo,
closeThread,
members,
userInfo,
secretKey,
getSecretKey
}: ThreadProps) => {
const [messages, setMessages] = useState<any[]>([])
const [hashMapMailMessages, setHashMapMailMessages] = useState({})
const secretKeyRef = useRef(null)
useEffect(() => {
secretKeyRef.current = secretKey;
}, [secretKey]);
const getIndividualMsg = async (message: any) => {
try {
const responseDataMessage = await getEncryptedResource({identifier: message.identifier, name: message.name, secretKey})
const fullObject = {
...message,
...(responseDataMessage || {}),
id: message.identifier
}
setHashMapMailMessages((prev)=> {
return {
...prev,
[message.identifier]: fullObject
}
})
} catch (error) {}
}
const getMailMessages = React.useCallback(
async (groupInfo: any, reset?: boolean, hideAlert?: boolean) => {
try {
if(!hideAlert){
// dispatch(setIsLoadingCustom('Loading messages'))
}
let threadId = groupInfo.threadId
const offset = messages.length
const identifier = `thmsg-${threadId}`
const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
const responseData = await response.json()
let fullArrayMsg = reset ? [] : [...messages]
let newMessages: any[] = []
for (const message of responseData) {
const index = fullArrayMsg.findIndex(
(p) => p.identifier === message.identifier
)
if (index !== -1) {
fullArrayMsg[index] = message
} else {
fullArrayMsg.push(message)
getIndividualMsg(message)
}
}
setMessages(fullArrayMsg)
} catch (error) {
} finally {
if(!hideAlert){
// dispatch(setIsLoadingCustom(null))
}
}
},
[messages, secretKey]
)
const getMessages = React.useCallback(async () => {
if (!currentThread || !secretKey) return
await getMailMessages(currentThread, true)
}, [getMailMessages, currentThread, secretKey])
const firstMount = useRef(false)
const saveTimestamp = useCallback((currentThread: any, username?: string)=> {
if(!currentThread?.threadData?.groupId || !currentThread?.threadId || !username) return
const threadIdForLocalStorage = `qmail_threads_${currentThread?.threadData?.groupId}_${currentThread?.threadId}`
const threads = JSON.parse(
localStorage.getItem(`qmail_threads_viewedtimestamp_${username}`) || "{}"
);
// Convert to an array of objects with identifier and all fields
let dataArray = Object.entries(threads).map(([identifier, value]) => ({
identifier,
...(value as any),
}));
// Sort the array based on timestamp in descending order
dataArray.sort((a, b) => b.timestamp - a.timestamp);
// Slice the array to keep only the first 500 elements
let latest500 = dataArray.slice(0, 500);
// Convert back to the original object format
let latest500Data: any = {};
latest500.forEach(item => {
const { identifier, ...rest } = item;
latest500Data[identifier] = rest;
});
latest500Data[threadIdForLocalStorage] = {
timestamp: Date.now(),
}
localStorage.setItem(
`qmail_threads_viewedtimestamp_${username}`,
JSON.stringify(latest500Data)
);
}, [])
useEffect(() => {
if (currentThread && secretKey) {
getMessages()
firstMount.current = true
// saveTimestamp(currentThread, user.name)
}
}, [ currentThread, secretKey])
const messageCallback = useCallback((msg: any) => {
// dispatch(addToHashMapMail(msg))
setMessages((prev) => [msg, ...prev])
}, [])
const interval = useRef<any>(null)
const checkNewMessages = React.useCallback(
async (groupInfo: any) => {
try {
let threadId = groupInfo.threadId
const identifier = `thmsg-${threadId}`
const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
const responseData = await response.json()
const latestMessage = messages[0]
if (!latestMessage) return
const findMessage = responseData?.findIndex(
(item: any) => item?.identifier === latestMessage?.identifier
)
let sliceLength = responseData.length
if (findMessage !== -1) {
sliceLength = findMessage
}
const newArray = responseData.slice(0, findMessage).reverse()
let fullArrayMsg = [...messages]
for (const message of newArray) {
try {
const responseDataMessage = await getEncryptedResource({identifier: message.identifier, name: message.name, secretKey: secretKeyRef.current})
const fullObject = {
...message,
...(responseDataMessage || {}),
id: message.identifier
}
setHashMapMailMessages((prev)=> {
return {
...prev,
[message.identifier]: fullObject
}
})
const index = messages.findIndex(
(p) => p.identifier === fullObject.identifier
)
if (index !== -1) {
fullArrayMsg[index] = fullObject
} else {
fullArrayMsg.unshift(fullObject)
}
} catch (error) {}
}
setMessages(fullArrayMsg)
} catch (error) {
} finally {
}
},
[messages]
)
const checkNewMessagesFunc = useCallback(() => {
let isCalling = false
interval.current = setInterval(async () => {
if (isCalling) return
isCalling = true
const res = await checkNewMessages(currentThread)
isCalling = false
}, 8000)
}, [checkNewMessages, currentThread])
useEffect(() => {
checkNewMessagesFunc()
return () => {
if (interval?.current) {
clearInterval(interval.current)
}
}
}, [checkNewMessagesFunc])
if (!currentThread) return null
return (
<GroupContainer
sx={{
position: "relative",
overflow: 'auto',
width: '100%'
}}
>
<NewThread
groupInfo={groupInfo}
isMessage={true}
currentThread={currentThread}
messageCallback={messageCallback}
members={members}
userInfo={userInfo}
getSecretKey={getSecretKey}
/>
<ThreadContainerFullWidth>
<ThreadContainer>
<Spacer height="30px" />
<Box sx={{
width: '100%',
alignItems: 'center',
display: 'flex',
justifyContent: 'space-between'
}}>
<GroupNameP>{currentThread?.threadData?.title}</GroupNameP>
<ShowMessageReturnButton onClick={() => {
setMessages([])
closeThread()
}}>
<MailIconImg src={ReturnSVG} />
<ComposeP>Return to Threads</ComposeP>
</ShowMessageReturnButton>
</Box>
<Spacer height="60px" />
{messages.map((message) => {
let fullMessage = message
if (hashMapMailMessages[message?.identifier]) {
fullMessage = hashMapMailMessages[message.identifier]
return <ShowMessage key={message?.identifier} message={fullMessage} />
}
return (
<SingleThreadParent>
<Skeleton
variant="rectangular"
style={{
width: '100%',
height: 60,
borderRadius: '8px',
overflow: 'hidden'
}}
/>
</SingleThreadParent>
)
})}
</ThreadContainer>
</ThreadContainerFullWidth>
{messages.length >= 20 && (
<LazyLoad onLoadMore={()=> getMailMessages(currentThread, false, true)}></LazyLoad>
)}
</GroupContainer>
)
}

View File

@@ -0,0 +1,663 @@
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Box, Button, IconButton, Skeleton } from "@mui/material";
import { ShowMessage } from "./ShowMessageWithoutModal";
// import {
// setIsLoadingCustom,
// } from '../../state/features/globalSlice'
import {
ComposeP,
GroupContainer,
GroupNameP,
MailIconImg,
ShowMessageReturnButton,
SingleThreadParent,
ThreadContainer,
ThreadContainerFullWidth,
} from "./Mail-styles";
import { Spacer } from "../../../common/Spacer";
import { threadIdentifier } from "./GroupMail";
import LazyLoad from "../../../common/LazyLoad";
import ReturnSVG from "../../../assets/svgs/Return.svg";
import { NewThread } from "./NewThread";
import { decryptPublishes, getTempPublish } from "../../Chat/GroupAnnouncements";
import { LoadingSnackbar } from "../../Snackbar/LoadingSnackbar";
import { subscribeToEvent, unsubscribeFromEvent } from "../../../utils/events";
import RefreshIcon from "@mui/icons-material/Refresh";
import { getBaseApi } from "../../../background";
import { getBaseApiReact } from "../../../App";
interface ThreadProps {
currentThread: any;
groupInfo: any;
closeThread: () => void;
members: any;
}
const getEncryptedResource = async ({ name, identifier, secretKey }) => {
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64`
);
const data = await res.text();
const response = await decryptPublishes([{ data }], secretKey);
const messageData = response[0];
return messageData.decryptedData;
};
export const Thread = ({
currentThread,
groupInfo,
closeThread,
members,
userInfo,
secretKey,
getSecretKey,
updateThreadActivityCurrentThread
}: ThreadProps) => {
const [tempPublishedList, setTempPublishedList] = useState([])
const [messages, setMessages] = useState<any[]>([]);
const [hashMapMailMessages, setHashMapMailMessages] = useState({});
const [hasFirstPage, setHasFirstPage] = useState(false);
const [hasPreviousPage, setHasPreviousPage] = useState(false);
const [hasNextPage, setHasNextPage] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [postReply, setPostReply] = useState(null);
const [hasLastPage, setHasLastPage] = useState(false);
const secretKeyRef = useRef(null);
const currentThreadRef = useRef(null);
const containerRef = useRef(null);
useEffect(() => {
currentThreadRef.current = currentThread;
}, [currentThread]);
useEffect(() => {
secretKeyRef.current = secretKey;
}, [secretKey]);
const getIndividualMsg = async (message: any) => {
try {
const responseDataMessage = await getEncryptedResource({
identifier: message.identifier,
name: message.name,
secretKey,
});
const fullObject = {
...message,
...(responseDataMessage || {}),
id: message.identifier,
};
setHashMapMailMessages((prev) => {
return {
...prev,
[message.identifier]: fullObject,
};
});
} catch (error) {}
};
const setTempData = async ()=> {
try {
let threadId = currentThread.threadId;
const keyTemp = 'thread-post'
const getTempAnnouncements = await getTempPublish()
if(getTempAnnouncements?.[keyTemp]){
let tempData = []
Object.keys(getTempAnnouncements?.[keyTemp] || {}).map((key)=> {
const value = getTempAnnouncements?.[keyTemp][key]
if(value.data?.threadId === threadId){
tempData.push(value.data)
}
})
setTempPublishedList(tempData)
}
} catch (error) {
}
}
const getMailMessages = React.useCallback(
async (groupInfo: any, before, after, isReverse) => {
try {
setTempPublishedList([])
setIsLoading(true);
setHasFirstPage(false);
setHasPreviousPage(false);
setHasLastPage(false);
setHasNextPage(false);
let threadId = groupInfo.threadId;
const identifier = `thmsg-${threadId}`;
let url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&prefix=true`;
if (!isReverse) {
url = url + "&reverse=false";
}
if (isReverse) {
url = url + "&reverse=true";
}
if (after) {
url = url + `&after=${after}`;
}
if (before) {
url = url + `&before=${before}`;
}
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
let fullArrayMsg = [...responseData];
if (isReverse) {
fullArrayMsg = fullArrayMsg.reverse();
}
// let newMessages: any[] = []
for (const message of responseData) {
getIndividualMsg(message);
}
setMessages(fullArrayMsg);
if (before === null && after === null && isReverse) {
setTimeout(() => {
containerRef.current.scrollIntoView({ behavior: "smooth" });
}, 300);
}
if (fullArrayMsg.length === 0){
setTempData()
return;
}
// check if there are newer posts
const urlNewer = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=1&includemetadata=false&reverse=false&prefix=true&before=${fullArrayMsg[0].created}`;
const responseNewer = await fetch(urlNewer, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseDataNewer = await responseNewer.json();
if (responseDataNewer.length > 0) {
setHasFirstPage(true);
setHasPreviousPage(true);
} else {
setHasFirstPage(false);
setHasPreviousPage(false);
}
// check if there are older posts
const urlOlder = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=1&includemetadata=false&reverse=false&prefix=true&after=${
fullArrayMsg[fullArrayMsg.length - 1].created
}`;
const responseOlder = await fetch(urlOlder, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseDataOlder = await responseOlder.json();
if (responseDataOlder.length > 0) {
setHasLastPage(true);
setHasNextPage(true);
} else {
setHasLastPage(false);
setHasNextPage(false);
setTempData()
updateThreadActivityCurrentThread()
}
} catch (error) {
} finally {
setIsLoading(false);
}
},
[messages, secretKey]
);
const getMessages = React.useCallback(async () => {
if (!currentThread || !secretKey) return;
await getMailMessages(currentThread, null, null, false);
}, [getMailMessages, currentThread, secretKey]);
const firstMount = useRef(false);
const saveTimestamp = useCallback((currentThread: any, username?: string) => {
if (
!currentThread?.threadData?.groupId ||
!currentThread?.threadId ||
!username
)
return;
const threadIdForLocalStorage = `qmail_threads_${currentThread?.threadData?.groupId}_${currentThread?.threadId}`;
const threads = JSON.parse(
localStorage.getItem(`qmail_threads_viewedtimestamp_${username}`) || "{}"
);
// Convert to an array of objects with identifier and all fields
let dataArray = Object.entries(threads).map(([identifier, value]) => ({
identifier,
...(value as any),
}));
// Sort the array based on timestamp in descending order
dataArray.sort((a, b) => b.timestamp - a.timestamp);
// Slice the array to keep only the first 500 elements
let latest500 = dataArray.slice(0, 500);
// Convert back to the original object format
let latest500Data: any = {};
latest500.forEach((item) => {
const { identifier, ...rest } = item;
latest500Data[identifier] = rest;
});
latest500Data[threadIdForLocalStorage] = {
timestamp: Date.now(),
};
localStorage.setItem(
`qmail_threads_viewedtimestamp_${username}`,
JSON.stringify(latest500Data)
);
}, []);
const getMessagesMiddleware = async () => {
await new Promise((res) => {
setTimeout(() => {
res(null);
}, 400);
});
if (firstMount.current) return;
getMessages();
firstMount.current = true;
};
useEffect(() => {
if (currentThreadRef.current?.threadId !== currentThread?.threadId) {
firstMount.current = false;
}
if (currentThread && secretKey && !firstMount.current) {
getMessagesMiddleware();
// saveTimestamp(currentThread, user.name)
}
}, [currentThread, secretKey]);
const messageCallback = useCallback((msg: any) => {
// dispatch(addToHashMapMail(msg))
// setMessages((prev) => [msg, ...prev])
}, []);
const interval = useRef<any>(null);
const checkNewMessages = React.useCallback(
async (groupInfo: any) => {
try {
let threadId = groupInfo.threadId;
const identifier = `thmsg-${threadId}`;
const url = `${getBaseApiReact()}/arbitrary/resources/search?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
const latestMessage = messages[0];
if (!latestMessage) return;
const findMessage = responseData?.findIndex(
(item: any) => item?.identifier === latestMessage?.identifier
);
let sliceLength = responseData.length;
if (findMessage !== -1) {
sliceLength = findMessage;
}
const newArray = responseData.slice(0, findMessage).reverse();
let fullArrayMsg = [...messages];
for (const message of newArray) {
try {
const responseDataMessage = await getEncryptedResource({
identifier: message.identifier,
name: message.name,
secretKey: secretKeyRef.current,
});
const fullObject = {
...message,
...(responseDataMessage || {}),
id: message.identifier,
};
setHashMapMailMessages((prev) => {
return {
...prev,
[message.identifier]: fullObject,
};
});
const index = messages.findIndex(
(p) => p.identifier === fullObject.identifier
);
if (index !== -1) {
fullArrayMsg[index] = fullObject;
} else {
fullArrayMsg.unshift(fullObject);
}
} catch (error) {}
}
setMessages(fullArrayMsg);
} catch (error) {
} finally {
}
},
[messages]
);
// const checkNewMessagesFunc = useCallback(() => {
// let isCalling = false
// interval.current = setInterval(async () => {
// if (isCalling) return
// isCalling = true
// const res = await checkNewMessages(currentThread)
// isCalling = false
// }, 8000)
// }, [checkNewMessages, currentThrefirstMount.current = truead])
// useEffect(() => {
// checkNewMessagesFunc()
// return () => {
// if (interval?.current) {
// clearInterval(interval.current)
// }
// }
// }, [checkNewMessagesFunc])
const openNewPostWithQuote = useCallback((reply) => {
setPostReply(reply);
}, []);
const closeCallback = useCallback(() => {
setPostReply(null);
}, []);
const threadFetchModeFunc = (e) => {
const mode = e.detail?.mode;
if (mode === "last-page") {
getMailMessages(currentThread, null, null, true);
}
firstMount.current = true;
};
React.useEffect(() => {
subscribeToEvent("threadFetchMode", threadFetchModeFunc);
return () => {
unsubscribeFromEvent("threadFetchMode", threadFetchModeFunc);
};
}, []);
const combinedListTempAndReal = useMemo(() => {
// Combine the two lists
const combined = [...tempPublishedList, ...messages];
// Remove duplicates based on the "identifier"
const uniqueItems = new Map();
combined.forEach(item => {
uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence
});
// Convert the map back to an array and sort by "created" timestamp in descending order
const sortedList = Array.from(uniqueItems.values()).sort((a, b) => a.created - b.created);
return sortedList;
}, [tempPublishedList, messages]);
if (!currentThread) return null;
return (
<GroupContainer
sx={{
position: "relative",
overflow: "auto",
width: "100%",
}}
>
<NewThread
groupInfo={groupInfo}
isMessage={true}
currentThread={currentThread}
messageCallback={messageCallback}
members={members}
userInfo={userInfo}
getSecretKey={getSecretKey}
closeCallback={closeCallback}
postReply={postReply}
myName={userInfo?.name}
publishCallback={setTempData}
/>
<ThreadContainerFullWidth>
<ThreadContainer >
<Spacer height="30px" />
<Box
sx={{
width: "100%",
alignItems: "center",
display: "flex",
justifyContent: "space-between",
}}
>
<GroupNameP>{currentThread?.threadData?.title}</GroupNameP>
<ShowMessageReturnButton
onClick={() => {
setMessages([]);
closeThread();
}}
>
<MailIconImg src={ReturnSVG} />
<ComposeP>Return to Threads</ComposeP>
</ShowMessageReturnButton>
</Box>
<Box
sx={{
width: "100%",
alignItems: "center",
display: "flex",
justifyContent: "center",
gap: "5px",
}}
>
<Button
onClick={() => {
getMailMessages(currentThread, null, null, false);
}}
disabled={!hasFirstPage}
variant="contained"
>
First Page
</Button>
<Button
onClick={() => {
getMailMessages(
currentThread,
messages[0].created,
null,
false
);
}}
disabled={!hasPreviousPage}
variant="contained"
>
Previous Page
</Button>
<Button
onClick={() => {
getMailMessages(
currentThread,
null,
messages[messages.length - 1].created,
false
);
}}
disabled={!hasNextPage}
variant="contained"
>
Next page
</Button>
<Button
onClick={() => {
getMailMessages(currentThread, null, null, true);
}}
disabled={!hasLastPage}
variant="contained"
>
Last page
</Button>
</Box>
<Spacer height="60px" />
{combinedListTempAndReal.map((message) => {
let fullMessage = message;
if (hashMapMailMessages[message?.identifier]) {
fullMessage = hashMapMailMessages[message.identifier];
return (
<ShowMessage
key={message?.identifier}
message={fullMessage}
openNewPostWithQuote={openNewPostWithQuote}
/>
);
} else if(message?.tempData){
return (
<ShowMessage
key={message?.identifier}
message={message?.tempData}
openNewPostWithQuote={openNewPostWithQuote}
/>
);
}
return (
<SingleThreadParent>
<Skeleton
variant="rectangular"
style={{
width: "100%",
height: 60,
borderRadius: "8px",
overflow: "hidden",
}}
/>
</SingleThreadParent>
);
})}
<div ref={containerRef} />
{!hasLastPage && !isLoading && (
<>
<Spacer height="20px" />
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "flex-end",
}}
>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={() => {
getMailMessages(currentThread, null, null, true);
}}
sx={{
color: "white",
}}
>
Refetch page
</Button>
</Box>
</>
)}
{messages?.length > 4 && (
<>
<Spacer height="30px" />
<Box
sx={{
width: "100%",
alignItems: "center",
display: "flex",
justifyContent: "center",
gap: "5px",
}}
>
<Button
onClick={() => {
getMailMessages(currentThread, null, null, false);
}}
disabled={!hasFirstPage}
variant="contained"
>
First Page
</Button>
<Button
onClick={() => {
getMailMessages(
currentThread,
messages[0].created,
null,
false
);
}}
disabled={!hasPreviousPage}
variant="contained"
>
Previous Page
</Button>
<Button
onClick={() => {
getMailMessages(
currentThread,
null,
messages[messages.length - 1].created,
false
);
}}
disabled={!hasNextPage}
variant="contained"
>
Next page
</Button>
<Button
onClick={() => {
getMailMessages(currentThread, null, null, true);
}}
disabled={!hasLastPage}
variant="contained"
>
Last page
</Button>
</Box>
<Spacer height="30px" />
</>
)}
</ThreadContainer>
</ThreadContainerFullWidth>
{/* {messages.length >= 20 && (
<LazyLoad onLoadMore={()=> getMailMessages(currentThread, false, true)}></LazyLoad>
)} */}
<LoadingSnackbar
open={isLoading}
info={{
message: "Loading posts... please wait.",
}}
/>
</GroupContainer>
);
};

View File

@@ -0,0 +1,71 @@
.ql-editor {
min-height: 200px;
width: 100%;
color: black;
font-size: 16px;
font-family: Roboto;
max-height: 225px;
overflow-y: scroll;
padding: 0px !important;
}
.ql-editor::-webkit-scrollbar-track {
background-color: transparent;
cursor: default;
}
.ql-editor::-webkit-scrollbar-track:hover {
background-color: transparent;
}
.ql-editor::-webkit-scrollbar {
width: 16px;
height: 10px;
background-color: rgba(229, 229, 229, 0.70);
}
.ql-editor::-webkit-scrollbar-thumb {
background-color: #B0B0B0;
border-radius: 8px;
background-clip: content-box;
border: 4px solid transparent;
}
.ql-editor img {
cursor: default;
}
.ql-editor-display {
min-height: 20px;
width: 100%;
color: black;
font-size: 16px;
font-family: Roboto;
padding: 0px !important;
}
.ql-editor-display img {
cursor: default;
}
.ql-container {
font-size: 16px
}
.ql-toolbar .ql-stroke {
fill: none !important;
stroke: black !important;
}
.ql-toolbar .ql-fill {
fill: black !important;
stroke: none !important;
}
.ql-toolbar .ql-picker {
color: black !important;
}
.ql-toolbar .ql-picker-options {
background-color: white !important;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
import * as React from "react";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Checkbox from "@mui/material/Checkbox";
import IconButton from "@mui/material/IconButton";
import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info";
import GroupAddIcon from '@mui/icons-material/GroupAdd';
import { executeEvent } from "../../utils/events";
import { Box, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import { getGroupNames } from "./UserListOfInvites";
import { CustomLoader } from "../../common/CustomLoader";
import { getBaseApiReact } from "../../App";
export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([])
const [loading, setLoading] = React.useState(true)
const getJoinRequests = async ()=> {
try {
setLoading(true)
const response = await fetch(`${getBaseApiReact()}/groups/invites/${myAddress}/?limit=0`);
const data = await response.json();
const resMoreData = await getGroupNames(data)
setGroupsWithJoinRequests(resMoreData)
} catch (error) {
} finally {
setLoading(false)
}
}
React.useEffect(() => {
if (myAddress) {
getJoinRequests()
}
}, [myAddress]);
return (
<Box sx={{
width: '360px',
display: 'flex',
flexDirection: 'column',
bgcolor: "background.paper",
padding: '20px'
}}>
<Typography sx={{
fontSize: '14px'
}}>Group Invites</Typography>
<Spacer height="10px" />
{loading && groupsWithJoinRequests.length === 0 && (
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<CustomLoader />
</Box>
)}
{!loading && groupsWithJoinRequests.length === 0 && (
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<Typography sx={{
fontSize: '12px'
}}>No invites</Typography>
</Box>
)}
<List sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper", maxHeight: '300px', overflow: 'auto' }}>
{groupsWithJoinRequests?.map((group)=> {
return (
<ListItem
key={group?.groupId}
onClick={()=> {
setOpenAddGroup(true)
setTimeout(() => {
executeEvent("openGroupInvitesRequest", {});
}, 300);
}}
disablePadding
secondaryAction={
<IconButton edge="end" aria-label="comments">
<GroupAddIcon
sx={{
color: "white",
}}
/>
</IconButton>
}
>
<ListItemButton disableRipple role={undefined} dense>
<ListItemText primary={`${group?.groupName} has invited you`} />
</ListItemButton>
</ListItem>
)
})}
</List>
</Box>
);
};

View File

@@ -0,0 +1,170 @@
import * as React from "react";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Checkbox from "@mui/material/Checkbox";
import IconButton from "@mui/material/IconButton";
import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info";
import { RequestQueueWithPromise } from "../../utils/queue/queue";
import GroupAddIcon from '@mui/icons-material/GroupAdd';
import { executeEvent } from "../../utils/events";
import { Box, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import { CustomLoader } from "../../common/CustomLoader";
import { getBaseApi } from "../../background";
import { getBaseApiReact } from "../../App";
export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(3)
export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection }) => {
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([])
const [loading, setLoading] = React.useState(true)
const getJoinRequests = async ()=> {
try {
setLoading(true)
let groupsAsAdmin = []
const getAllGroupsAsAdmin = groups.map(async (group)=> {
const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
return fetch(
`${getBaseApiReact()}/groups/members/${group.groupId}?limit=0&onlyAdmins=true`
);
})
const isAdminData = await isAdminResponse.json()
const findMyself = isAdminData?.members?.find((member)=> member.member === myAddress)
if(findMyself){
groupsAsAdmin.push(group)
}
return true
})
// const getJoinGroupRequests = groupsAsAdmin.map(async (group)=> {
// console.log('getJoinGroupRequests', group)
// const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
// return fetch(
// `${getBaseApiReact()}/groups/joinrequests/${group.groupId}`
// );
// })
// const joinRequestData = await joinRequestResponse.json()
// return {
// group,
// data: joinRequestData
// }
// })
await Promise.all(getAllGroupsAsAdmin)
const res = await Promise.all(groupsAsAdmin.map(async (group)=> {
const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
return fetch(
`${getBaseApiReact()}/groups/joinrequests/${group.groupId}`
);
})
const joinRequestData = await joinRequestResponse.json()
return {
group,
data: joinRequestData
}
}))
setGroupsWithJoinRequests(res)
} catch (error) {
} finally {
setLoading(false)
}
}
React.useEffect(() => {
if (myAddress && groups.length > 0) {
getJoinRequests()
} else {
setLoading(false)
}
}, [myAddress, groups]);
return (
<Box sx={{
width: '360px',
display: 'flex',
flexDirection: 'column',
bgcolor: "background.paper",
padding: '20px'
}}>
<Typography sx={{
fontSize: '14px'
}}>Join Requests</Typography>
<Spacer height="10px" />
{loading && groupsWithJoinRequests.length === 0 && (
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<CustomLoader />
</Box>
)}
{!loading && groupsWithJoinRequests.length === 0 && (
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<Typography sx={{
fontSize: '12px'
}}>No join requests</Typography>
</Box>
)}
<List sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper", maxHeight: '300px', overflow: 'auto' }}>
{groupsWithJoinRequests?.map((group)=> {
if(group?.data?.length === 0) return null
return (
<ListItem
key={group?.groupId}
onClick={()=> {
setSelectedGroup(group?.group)
getTimestampEnterChat()
setGroupSection("announcement")
setOpenManageMembers(true)
setTimeout(() => {
executeEvent("openGroupJoinRequest", {});
}, 300);
}}
disablePadding
secondaryAction={
<IconButton edge="end" aria-label="comments">
<GroupAddIcon
sx={{
color: "white",
}}
/>
</IconButton>
}
>
<ListItemButton disableRipple role={undefined} dense>
<ListItemText primary={`${group?.group?.groupName} has ${group?.data?.length} pending join requests.`} />
</ListItemButton>
</ListItem>
)
})}
</List>
</Box>
);
};

View File

@@ -0,0 +1,108 @@
import { LoadingButton } from "@mui/lab";
import {
Box,
Button,
Input,
MenuItem,
Select,
SelectChangeEvent,
} from "@mui/material";
import React, { useState } from "react";
import { Spacer } from "../../common/Spacer";
import { Label } from "./AddGroup";
import { getFee } from "../../background";
export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
const [value, setValue] = useState("");
const [expiryTime, setExpiryTime] = useState<string>('259200');
const [isLoadingInvite, setIsLoadingInvite] = useState(false)
const inviteMember = async () => {
try {
const fee = await getFee('GROUP_INVITE')
await show({
message: "Would you like to perform a GROUP_INVITE transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoadingInvite(true)
if (!expiryTime || !value) return;
new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "inviteToGroup",
payload: {
groupId,
qortalAddress: value,
inviteTime: +expiryTime,
},
},
(response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: `Successfully invited ${value}. It may take a couple of minutes for the changes to propagate`,
});
setOpenSnack(true);
res(response);
setValue("");
return
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error);
}
);
});
} catch (error) {} finally {
setIsLoadingInvite(false)
}
};
const handleChange = (event: SelectChangeEvent) => {
setExpiryTime(event.target.value as string);
};
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
>
Invite member
<Spacer height="20px" />
<Input
value={value}
placeholder="Name or address"
onChange={(e) => setValue(e.target.value)}
/>
<Spacer height="20px" />
<Label>Invitation Expiry Time</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={expiryTime}
label="Invitation Expiry Time"
onChange={handleChange}
>
<MenuItem value={10800}>3 hours</MenuItem>
<MenuItem value={21600}>6 hours</MenuItem>
<MenuItem value={43200}>12 hours</MenuItem>
<MenuItem value={86400}>1 day</MenuItem>
<MenuItem value={259200}>3 days</MenuItem>
<MenuItem value={432000}>5 days</MenuItem>
<MenuItem value={604800}>7 days</MenuItem>
<MenuItem value={864000}>10 days</MenuItem>
<MenuItem value={1296000}>15 days</MenuItem>
<MenuItem value={2592000}>30 days</MenuItem>
</Select>
<Spacer height="20px" />
<LoadingButton variant="contained" loadingPosition="start" loading={isLoadingInvite} onClick={inviteMember}>Invite</LoadingButton>
</Box>
);
};

View File

@@ -0,0 +1,188 @@
import React, { useEffect, useRef, useState } from 'react';
import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
import { getNameInfo } from './Group';
import { getBaseApi, getFee } from '../../background';
import { LoadingButton } from '@mui/lab';
import { getBaseApiReact } from '../../App';
export const getMemberInvites = async (groupNumber) => {
const response = await fetch(`${getBaseApiReact()}/groups/bans/${groupNumber}?limit=0`);
const groupData = await response.json();
return groupData;
}
const getNames = async (listOfMembers) => {
let members = [];
if (listOfMembers && Array.isArray(listOfMembers)) {
for (const member of listOfMembers) {
if (member.offender) {
const name = await getNameInfo(member.offender);
if (name) {
members.push({ ...member, name });
}
}
}
}
return members;
}
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
const [bans, setBans] = useState([]);
const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to
const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open
const listRef = useRef();
const [isLoadingUnban, setIsLoadingUnban] = useState(false);
const getInvites = async (groupId) => {
try {
const res = await getMemberInvites(groupId);
const resWithNames = await getNames(res);
setBans(resWithNames);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
if (groupId) {
getInvites(groupId);
}
}, [groupId]);
const handlePopoverOpen = (event, index) => {
setPopoverAnchor(event.currentTarget);
setOpenPopoverIndex(index);
};
const handlePopoverClose = () => {
setPopoverAnchor(null);
setOpenPopoverIndex(null);
};
const handleCancelBan = async (address)=> {
try {
const fee = await getFee('CANCEL_GROUP_BAN')
await show({
message: "Would you like to perform a CANCEL_GROUP_BAN transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoadingUnban(true)
new Promise((res, rej)=> {
chrome.runtime.sendMessage({ action: "cancelBan", payload: {
groupId,
qortalAddress: address,
}}, (response) => {
if (!response?.error) {
res(response)
setIsLoadingUnban(false)
setInfoSnack({
type: "success",
message: "Successfully unbanned user. It may take a couple of minutes for the changes to propagate",
});
handlePopoverClose();
setOpenSnack(true);
return
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error)
});
})
} catch (error) {
} finally {
setIsLoadingUnban(false)
}
}
const rowRenderer = ({ index, key, parent, style }) => {
const member = bans[index];
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
{({ measure }) => (
<div style={style} onLoad={measure}>
<ListItem disablePadding>
<Popover
open={openPopoverIndex === index}
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
style={{ marginTop: "8px" }}
>
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<LoadingButton loading={isLoadingUnban}
loadingPosition="start"
variant="contained" onClick={()=> handleCancelBan(member?.offender)}>Cancel Ban</LoadingButton>
</Box>
</Popover>
<ListItemButton onClick={(event) => handlePopoverOpen(event, index)}>
<ListItemAvatar>
<Avatar
alt={member?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true`}
/>
</ListItemAvatar>
<ListItemText primary={member?.name || member?.offender} />
</ListItemButton>
</ListItem>
</div>
)}
</CellMeasurer>
);
};
return (
<div>
<p>Ban list</p>
<div style={{ position: 'relative', height: '500px', width: '600px', display: 'flex', flexDirection: 'column', flexShrink: 1 }}>
<AutoSizer>
{({ height, width }) => (
<List
ref={listRef}
width={width}
height={height}
rowCount={bans.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
deferredMeasurementCache={cache}
/>
)}
</AutoSizer>
</div>
</div>
);
}

View File

@@ -0,0 +1,189 @@
import React, { useEffect, useRef, useState } from 'react';
import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
import { getNameInfo } from './Group';
import { getBaseApi, getFee } from '../../background';
import { LoadingButton } from '@mui/lab';
import { getBaseApiReact } from '../../App';
export const getMemberInvites = async (groupNumber) => {
const response = await fetch(`${getBaseApiReact()}/groups/invites/group/${groupNumber}?limit=0`);
const groupData = await response.json();
return groupData;
}
const getNames = async (listOfMembers) => {
let members = [];
if (listOfMembers && Array.isArray(listOfMembers)) {
for (const member of listOfMembers) {
if (member.invitee) {
const name = await getNameInfo(member.invitee);
if (name) {
members.push({ ...member, name });
}
}
}
}
return members;
}
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
const [invites, setInvites] = useState([]);
const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to
const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open
const [isLoadingCancelInvite, setIsLoadingCancelInvite] = useState(false);
const listRef = useRef();
const getInvites = async (groupId) => {
try {
const res = await getMemberInvites(groupId);
const resWithNames = await getNames(res);
setInvites(resWithNames);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
if (groupId) {
getInvites(groupId);
}
}, [groupId]);
const handlePopoverOpen = (event, index) => {
setPopoverAnchor(event.currentTarget);
setOpenPopoverIndex(index);
};
const handlePopoverClose = () => {
setPopoverAnchor(null);
setOpenPopoverIndex(null);
};
const handleCancelInvitation = async (address)=> {
try {
const fee = await getFee('CANCEL_GROUP_INVITE')
await show({
message: "Would you like to perform a CANCEL_GROUP_INVITE transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoadingCancelInvite(true)
await new Promise((res, rej)=> {
chrome.runtime.sendMessage({ action: "cancelInvitationToGroup", payload: {
groupId,
qortalAddress: address,
}}, (response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: "Successfully canceled invitation. It may take a couple of minutes for the changes to propagate",
});
setOpenSnack(true);
handlePopoverClose();
setIsLoadingCancelInvite(true)
res(response)
return
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error)
});
})
} catch (error) {
} finally {
setIsLoadingCancelInvite(false)
}
}
const rowRenderer = ({ index, key, parent, style }) => {
const member = invites[index];
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
{({ measure }) => (
<div style={style} onLoad={measure}>
<ListItem disablePadding>
<Popover
open={openPopoverIndex === index}
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
style={{ marginTop: "8px" }}
>
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<LoadingButton loading={isLoadingCancelInvite}
loadingPosition="start"
variant="contained" onClick={()=> handleCancelInvitation(member?.invitee)}>Cancel Invitation</LoadingButton>
</Box>
</Popover>
<ListItemButton onClick={(event) => handlePopoverOpen(event, index)}>
<ListItemAvatar>
<Avatar
alt={member?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true`}
/>
</ListItemAvatar>
<ListItemText primary={member?.name || member?.invitee} />
</ListItemButton>
</ListItem>
</div>
)}
</CellMeasurer>
);
};
return (
<div>
<p>Invitees list</p>
<div style={{ position: 'relative', height: '500px', width: '600px', display: 'flex', flexDirection: 'column', flexShrink: 1 }}>
<AutoSizer>
{({ height, width }) => (
<List
ref={listRef}
width={width}
height={height}
rowCount={invites.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
deferredMeasurementCache={cache}
/>
)}
</AutoSizer>
</div>
</div>
);
}

View File

@@ -0,0 +1,189 @@
import React, { useEffect, useRef, useState } from 'react';
import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
import { getNameInfo } from './Group';
import { getBaseApi, getFee } from '../../background';
import { LoadingButton } from '@mui/lab';
import { getBaseApiReact } from '../../App';
export const getMemberInvites = async (groupNumber) => {
const response = await fetch(`${getBaseApiReact()}/groups/joinrequests/${groupNumber}?limit=0`);
const groupData = await response.json();
return groupData;
}
const getNames = async (listOfMembers) => {
let members = [];
if (listOfMembers && Array.isArray(listOfMembers)) {
for (const member of listOfMembers) {
if (member.joiner) {
const name = await getNameInfo(member.joiner);
if (name) {
members.push({ ...member, name });
}
}
}
}
return members;
}
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
const [invites, setInvites] = useState([]);
const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to
const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open
const listRef = useRef();
const [isLoadingAccept, setIsLoadingAccept] = useState(false);
const getInvites = async (groupId) => {
try {
const res = await getMemberInvites(groupId);
const resWithNames = await getNames(res);
setInvites(resWithNames);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
if (groupId) {
getInvites(groupId);
}
}, [groupId]);
const handlePopoverOpen = (event, index) => {
setPopoverAnchor(event.currentTarget);
setOpenPopoverIndex(index);
};
const handlePopoverClose = () => {
setPopoverAnchor(null);
setOpenPopoverIndex(null);
};
const handleAcceptJoinRequest = async (address)=> {
try {
const fee = await getFee('GROUP_INVITE')
await show({
message: "Would you like to perform a GROUP_INVITE transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoadingAccept(true)
await new Promise((res, rej)=> {
chrome.runtime.sendMessage({ action: "inviteToGroup", payload: {
groupId,
qortalAddress: address,
inviteTime: 10800,
}}, (response) => {
if (!response?.error) {
setIsLoadingAccept(false)
setInfoSnack({
type: "success",
message: "Successfully accepted join request. It may take a couple of minutes for the changes to propagate",
});
setOpenSnack(true);
handlePopoverClose();
res(response)
return
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error)
});
})
} catch (error) {
} finally {
setIsLoadingAccept(false)
}
}
const rowRenderer = ({ index, key, parent, style }) => {
const member = invites[index];
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
{({ measure }) => (
<div style={style} onLoad={measure}>
<ListItem disablePadding>
<Popover
open={openPopoverIndex === index}
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
style={{ marginTop: "8px" }}
>
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<LoadingButton loading={isLoadingAccept}
loadingPosition="start"
variant="contained" onClick={()=> handleAcceptJoinRequest(member?.joiner)}>Accept</LoadingButton>
</Box>
</Popover>
<ListItemButton onClick={(event) => handlePopoverOpen(event, index)}>
<ListItemAvatar>
<Avatar
alt={member?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true`}
/>
</ListItemAvatar>
<ListItemText primary={member?.name || member?.joiner} />
</ListItemButton>
</ListItem>
</div>
)}
</CellMeasurer>
);
};
return (
<div>
<p>Join request list</p>
<div style={{ position: 'relative', height: '500px', width: '600px', display: 'flex', flexDirection: 'column', flexShrink: 1 }}>
<AutoSizer>
{({ height, width }) => (
<List
ref={listRef}
width={width}
height={height}
rowCount={invites.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
deferredMeasurementCache={cache}
/>
)}
</AutoSizer>
</div>
</div>
);
}

View File

@@ -0,0 +1,385 @@
import {
Avatar,
Box,
Button,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemText,
Popover,
Typography,
} from "@mui/material";
import React, { useRef, useState } from "react";
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List,
} from "react-virtualized";
import { LoadingButton } from "@mui/lab";
import { getBaseApi, getFee } from "../../background";
import { getBaseApiReact } from "../../App";
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
const ListOfMembers = ({
members,
groupId,
setInfoSnack,
setOpenSnack,
isAdmin,
isOwner,
show,
}) => {
const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to
const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open
const [isLoadingKick, setIsLoadingKick] = useState(false);
const [isLoadingBan, setIsLoadingBan] = useState(false);
const [isLoadingMakeAdmin, setIsLoadingMakeAdmin] = useState(false);
const [isLoadingRemoveAdmin, setIsLoadingRemoveAdmin] = useState(false);
const listRef = useRef();
const handlePopoverOpen = (event, index) => {
setPopoverAnchor(event.currentTarget);
setOpenPopoverIndex(index);
};
const handlePopoverClose = () => {
setPopoverAnchor(null);
setOpenPopoverIndex(null);
};
const handleKick = async (address) => {
try {
const fee = await getFee("GROUP_KICK");
await show({
message: "Would you like to perform a GROUP_KICK transaction?",
publishFee: fee.fee + " QORT",
});
setIsLoadingKick(true);
new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "kickFromGroup",
payload: {
groupId,
qortalAddress: address,
},
},
(response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message:
"Successfully kicked member from group. It may take a couple of minutes for the changes to propagate",
});
setOpenSnack(true);
handlePopoverClose();
res(response);
return;
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error);
}
);
});
} catch (error) {
} finally {
setIsLoadingKick(false);
}
};
const handleBan = async (address) => {
try {
const fee = await getFee("GROUP_BAN");
await show({
message: "Would you like to perform a GROUP_BAN transaction?",
publishFee: fee.fee + " QORT",
});
setIsLoadingBan(true);
await new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "banFromGroup",
payload: {
groupId,
qortalAddress: address,
rBanTime: 0,
},
},
(response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message:
"Successfully banned member from group. It may take a couple of minutes for the changes to propagate",
});
setOpenSnack(true);
handlePopoverClose();
res(response);
return;
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error);
}
);
});
} catch (error) {
} finally {
setIsLoadingBan(false);
}
};
const makeAdmin = async (address) => {
try {
const fee = await getFee("ADD_GROUP_ADMIN");
await show({
message: "Would you like to perform a ADD_GROUP_ADMIN transaction?",
publishFee: fee.fee + " QORT",
});
setIsLoadingMakeAdmin(true);
await new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "makeAdmin",
payload: {
groupId,
qortalAddress: address,
},
},
(response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message:
"Successfully made member an admin. It may take a couple of minutes for the changes to propagate",
});
setOpenSnack(true);
handlePopoverClose();
res(response);
return;
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error);
}
);
});
} catch (error) {
} finally {
setIsLoadingMakeAdmin(false);
}
};
const removeAdmin = async (address) => {
try {
const fee = await getFee("REMOVE_GROUP_ADMIN");
await show({
message: "Would you like to perform a REMOVE_GROUP_ADMIN transaction?",
publishFee: fee.fee + " QORT",
});
setIsLoadingRemoveAdmin(true);
await new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "removeAdmin",
payload: {
groupId,
qortalAddress: address,
},
},
(response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message:
"Successfully removed member as an admin. It may take a couple of minutes for the changes to propagate",
});
setOpenSnack(true);
handlePopoverClose();
res(response);
return;
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error);
}
);
});
} catch (error) {
} finally {
setIsLoadingRemoveAdmin(false);
}
};
const rowRenderer = ({ index, key, parent, style }) => {
const member = members[index];
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
{({ measure }) => (
<div style={style} onLoad={measure}>
<Popover
open={openPopoverIndex === index}
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
style={{ marginTop: "8px" }}
>
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
{isOwner && (
<>
<LoadingButton
loading={isLoadingKick}
loadingPosition="start"
variant="contained"
onClick={() => handleKick(member?.member)}
>
Kick member from group
</LoadingButton>
<LoadingButton
loading={isLoadingBan}
loadingPosition="start"
variant="contained"
onClick={() => handleBan(member?.member)}
>
Ban member from group
</LoadingButton>
<LoadingButton
loading={isLoadingMakeAdmin}
loadingPosition="start"
variant="contained"
onClick={() => makeAdmin(member?.member)}
>
Make an admin
</LoadingButton>
<LoadingButton
loading={isLoadingRemoveAdmin}
loadingPosition="start"
variant="contained"
onClick={() => removeAdmin(member?.member)}
>
Remove as admin
</LoadingButton>
</>
)}
</Box>
</Popover>
<ListItem
key={member?.member}
// secondaryAction={
// <Checkbox
// edge="end"
// onChange={handleToggle(value)}
// checked={checked.indexOf(value) !== -1}
// inputProps={{ 'aria-labelledby': labelId }}
// />
// }
disablePadding
>
<ListItemButton
onClick={(event) => handlePopoverOpen(event, index)}
>
<ListItemAvatar>
<Avatar
alt={member?.name || member?.member}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true`}
/>
</ListItemAvatar>
<ListItemText
id={""}
primary={member?.name || member?.member}
/>
{member?.isAdmin && (
<Typography sx={{
color: 'white',
marginLeft: 'auto'
}}>Admin</Typography>
)}
</ListItemButton>
</ListItem>
</div>
)}
</CellMeasurer>
);
};
return (
<div>
<p>Member list</p>
<div
style={{
position: "relative",
height: "500px",
width: "600px",
display: "flex",
flexDirection: "column",
flexShrink: 1,
}}
>
<AutoSizer>
{({ height, width }) => (
<List
ref={listRef}
width={width}
height={height}
rowCount={members.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
// onScroll={handleScroll}
deferredMeasurementCache={cache}
/>
)}
</AutoSizer>
</div>
</div>
);
};
export default ListOfMembers;

View File

@@ -0,0 +1,138 @@
import * as React from "react";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Checkbox from "@mui/material/Checkbox";
import IconButton from "@mui/material/IconButton";
import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info";
import GroupAddIcon from '@mui/icons-material/GroupAdd';
import { executeEvent } from "../../utils/events";
import { Box, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import { getGroupNames } from "./UserListOfInvites";
import { CustomLoader } from "../../common/CustomLoader";
import VisibilityIcon from '@mui/icons-material/Visibility';
export const ListOfThreadPostsWatched = () => {
const [posts, setPosts] = React.useState([])
const [loading, setLoading] = React.useState(true)
const getPosts = async ()=> {
try {
await new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "getThreadActivity",
payload: {
},
},
(response) => {
if (!response?.error) {
if(!response) {
res(null)
return
}
const uniquePosts = response.reduce((acc, current) => {
const x = acc.find(item => item?.thread?.threadId === current?.thread?.threadId);
if (!x) {
return acc.concat([current]);
} else {
return acc;
}
}, []);
setPosts(uniquePosts)
res(uniquePosts);
return
}
rej(response.error);
}
);
});
} catch (error) {
} finally {
setLoading(false)
}
}
React.useEffect(() => {
getPosts()
}, []);
return (
<Box sx={{
width: '360px',
display: 'flex',
flexDirection: 'column',
bgcolor: "background.paper",
padding: '20px'
}}>
<Typography sx={{
fontSize: '14px'
}}>New Thread Posts</Typography>
<Spacer height="10px" />
{loading && posts.length === 0 && (
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<CustomLoader />
</Box>
)}
{!loading && posts.length === 0 && (
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<Typography sx={{
fontSize: '12px'
}}>No thread post notifications</Typography>
</Box>
)}
<List sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper", maxHeight: '300px', overflow: 'auto' }}>
{posts?.map((post)=> {
return (
<ListItem
key={post?.thread?.threadId}
onClick={()=> {
executeEvent("openThreadNewPost", {
data: post
});
}}
disablePadding
secondaryAction={
<IconButton edge="end" aria-label="comments">
<VisibilityIcon
sx={{
color: "red",
}}
/>
</IconButton>
}
>
<ListItemButton disableRipple role={undefined} dense>
<ListItemText primary={`New post in ${post?.thread?.threadData?.title}`} />
</ListItemButton>
</ListItem>
)
})}
</List>
</Box>
);
};

View File

@@ -0,0 +1,316 @@
import * as React from "react";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import ListItemText from "@mui/material/ListItemText";
import ListItemButton from "@mui/material/ListItemButton";
import List from "@mui/material/List";
import Divider from "@mui/material/Divider";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import CloseIcon from "@mui/icons-material/Close";
import Slide from "@mui/material/Slide";
import { TransitionProps } from "@mui/material/transitions";
import ListOfMembers from "./ListOfMembers";
import { InviteMember } from "./InviteMember";
import { ListOfInvites } from "./ListOfInvites";
import { ListOfBans } from "./ListOfBans";
import { ListOfJoinRequests } from "./ListOfJoinRequests";
import { Box, Tab, Tabs } from "@mui/material";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { MyContext } from "../../App";
import { getGroupMembers, getNames } from "./Group";
import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar";
import { getFee } from "../../background";
import { LoadingButton } from "@mui/lab";
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
}
const Transition = React.forwardRef(function Transition(
props: TransitionProps & {
children: React.ReactElement;
},
ref: React.Ref<unknown>
) {
return <Slide direction="up" ref={ref} {...props} />;
});
export const ManageMembers = ({
address,
open,
setOpen,
selectedGroup,
isAdmin,
isOwner
}) => {
const [membersWithNames, setMembersWithNames] = React.useState([]);
const [tab, setTab] = React.useState("create");
const [value, setValue] = React.useState(0);
const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null);
const [isLoadingMembers, setIsLoadingMembers] = React.useState(false)
const [isLoadingLeave, setIsLoadingLeave] = React.useState(false)
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};
const { show, setTxList } = React.useContext(MyContext);
const handleClose = () => {
setOpen(false);
};
const handleLeaveGroup = async () => {
try {
setIsLoadingLeave(true)
const fee = await getFee('LEAVE_GROUP')
await show({
message: "Would you like to perform an LEAVE_GROUP transaction?" ,
publishFee: fee.fee + ' QORT'
})
await new Promise((res, rej) => {
chrome.runtime.sendMessage(
{
action: "leaveGroup",
payload: {
groupId: selectedGroup?.groupId,
},
},
(response) => {
if (!response?.error) {
setTxList((prev)=> [{
...response,
type: 'leave-group',
label: `Left Group ${selectedGroup?.groupName}: awaiting confirmation`,
labelDone: `Left Group ${selectedGroup?.groupName}: success !`,
done: false,
groupId: selectedGroup?.groupId,
}, ...prev])
res(response);
setInfoSnack({
type: "success",
message: "Successfully requested to leave group. It may take a couple of minutes for the changes to propagate",
});
setOpenSnack(true);
return
}
rej(response.error);
}
);
});
} catch (error) {} finally {
setIsLoadingLeave(false)
}
};
const getMembers = async (groupId) => {
try {
setIsLoadingMembers(true)
const res = await getGroupMembers(groupId);
const resWithNames = await getNames(res.members);
setMembersWithNames(resWithNames);
setIsLoadingMembers(false)
} catch (error) {}
};
React.useEffect(()=> {
if(selectedGroup?.groupId){
getMembers(selectedGroup?.groupId)
}
}, [selectedGroup?.groupId])
const openGroupJoinRequestFunc = ()=> {
setValue(4)
}
React.useEffect(() => {
subscribeToEvent("openGroupJoinRequest", openGroupJoinRequestFunc);
return () => {
unsubscribeFromEvent("openGroupJoinRequest", openGroupJoinRequestFunc);
};
}, []);
return (
<React.Fragment>
<Dialog
fullScreen
open={open}
onClose={handleClose}
TransitionComponent={Transition}
>
<AppBar sx={{ position: "relative", bgcolor: "#232428" }}>
<Toolbar>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
Manage Members
</Typography>
<IconButton
edge="start"
color="inherit"
onClick={handleClose}
aria-label="close"
>
<CloseIcon />
</IconButton>
</Toolbar>
</AppBar>
<Box
sx={{
bgcolor: "#27282c",
flexGrow: 1,
overflowY: "auto",
color: "white",
}}
>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
sx={{
"& .MuiTabs-indicator": {
backgroundColor: "white",
},
}}
value={value}
onChange={handleChange}
aria-label="basic tabs example"
>
<Tab
sx={{
"&.Mui-selected": {
color: `white`,
},
}}
label="List of members"
{...a11yProps(0)}
/>
<Tab
sx={{
"&.Mui-selected": {
color: `white`,
},
}}
label="Invite new member"
{...a11yProps(1)}
/>
<Tab
sx={{
"&.Mui-selected": {
color: `white`,
},
}}
label="List of invites"
{...a11yProps(2)}
/>
<Tab
sx={{
"&.Mui-selected": {
color: `white`,
},
}}
label="List of bans"
{...a11yProps(3)}
/>
<Tab
sx={{
"&.Mui-selected": {
color: `white`,
},
}}
label="Join requests"
{...a11yProps(4)}
/>
</Tabs>
</Box>
{selectedGroup?.groupId && !isOwner && (
<LoadingButton loading={isLoadingLeave} loadingPosition="start"
variant="contained" onClick={handleLeaveGroup}>
Leave Group
</LoadingButton>
)}
{value === 0 && (
<Box
sx={{
width: "100%",
padding: "25px",
}}
>
<ListOfMembers
members={membersWithNames || []}
groupId={selectedGroup?.groupId}
setOpenSnack={setOpenSnack}
setInfoSnack={setInfoSnack}
isAdmin={isAdmin}
isOwner={isOwner}
show={show}
/>
</Box>
)}
{value === 1 && (
<Box
sx={{
width: "100%",
padding: "25px",
}}
>
<InviteMember show={show} groupId={selectedGroup?.groupId} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} />
</Box>
)}
{value === 2 && (
<Box
sx={{
width: "100%",
padding: "25px",
}}
>
<ListOfInvites show={show} groupId={selectedGroup?.groupId} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} />
</Box>
)}
{value === 3 && (
<Box
sx={{
width: "100%",
padding: "25px",
}}
>
<ListOfBans show={show} groupId={selectedGroup?.groupId} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} />
</Box>
)}
{value === 4 && (
<Box
sx={{
width: "100%",
padding: "25px",
}}
>
<ListOfJoinRequests show={show} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} groupId={selectedGroup?.groupId} />
</Box>
)}
</Box>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
<LoadingSnackbar
open={isLoadingMembers}
info={{
message: "Loading member list with names... please wait.",
}}
/>
</Dialog>
</React.Fragment>
);
};

View File

@@ -0,0 +1,166 @@
import * as React from "react";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Checkbox from "@mui/material/Checkbox";
import IconButton from "@mui/material/IconButton";
import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info";
import { Box, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance }) => {
const [checked1, setChecked1] = React.useState(false);
const [checked2, setChecked2] = React.useState(false);
const [checked3, setChecked3] = React.useState(false);
// const getAddressInfo = async (address) => {
// const response = await fetch(getBaseApiReact() + "/addresses/" + address);
// const data = await response.json();
// if (data.error && data.error === 124) {
// setChecked1(false);
// } else if (data.address) {
// setChecked1(true);
// }
// };
// const checkInfo = async () => {
// try {
// getAddressInfo(myAddress);
// } catch (error) {}
// };
React.useEffect(() => {
if (balance && +balance >= 6) {
setChecked1(true)
}
}, [balance]);
React.useEffect(()=> {
if(hasGroups) setChecked3(true)
}, [hasGroups])
React.useEffect(()=> {
if(name) setChecked2(true)
}, [name])
return (
<Box sx={{
width: '360px',
display: 'flex',
flexDirection: 'column',
bgcolor: "background.paper",
padding: '20px'
}}>
<Typography sx={{
fontSize: '14px'
}}>Suggestion: Complete the following</Typography>
<Spacer height="10px" />
<List sx={{ width: "100%", maxWidth: 360 }}>
<ListItem
// secondaryAction={
// <IconButton edge="end" aria-label="comments">
// <InfoIcon
// sx={{
// color: "white",
// }}
// />
// </IconButton>
// }
disablePadding
>
<ListItemButton disableRipple role={undefined} dense>
<ListItemIcon>
<Checkbox
edge="start"
checked={checked1}
tabIndex={-1}
disableRipple
disabled={true}
sx={{
"&.Mui-checked": {
color: "white", // Customize the color when checked
},
"& .MuiSvgIcon-root": {
color: "white",
},
}}
/>
</ListItemIcon>
<ListItemText primary={`Have at least 6 QORT in your wallet`} />
</ListItemButton>
</ListItem>
<ListItem
// secondaryAction={
// <IconButton edge="end" aria-label="comments">
// <InfoIcon
// sx={{
// color: "white",
// }}
// />
// </IconButton>
// }
disablePadding
>
<ListItemButton disableRipple role={undefined} dense>
<ListItemIcon>
<Checkbox
edge="start"
checked={checked2}
tabIndex={-1}
disableRipple
disabled={true}
sx={{
"&.Mui-checked": {
color: "white", // Customize the color when checked
},
"& .MuiSvgIcon-root": {
color: "white",
},
}}
/>
</ListItemIcon>
<ListItemText primary={`Register a name`} />
</ListItemButton>
</ListItem>
<ListItem
// secondaryAction={
// <IconButton edge="end" aria-label="comments">
// <InfoIcon
// sx={{
// color: "white",
// }}
// />
// </IconButton>
// }
disablePadding
>
<ListItemButton disableRipple role={undefined} dense>
<ListItemIcon>
<Checkbox
edge="start"
checked={checked3}
tabIndex={-1}
disableRipple
disabled={true}
sx={{
"&.Mui-checked": {
color: "white", // Customize the color when checked
},
"& .MuiSvgIcon-root": {
color: "white",
},
}}
/>
</ListItemIcon>
<ListItemText primary={`Join a group`} />
</ListItemButton>
</ListItem>
</List>
</Box>
);
};

View File

@@ -0,0 +1,206 @@
import { Box, Button, ListItem, ListItemButton, ListItemText, Popover, Typography } from '@mui/material';
import React, { useContext, useEffect, useRef, useState } from 'react'
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
import { MyContext, getBaseApiReact } from '../../App';
import { LoadingButton } from '@mui/lab';
import { getBaseApi, getFee } from '../../background';
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
const getGroupInfo = async (groupId)=> {
const response = await fetch(`${getBaseApiReact()}/groups/` + groupId);
const groupData = await response.json();
if (groupData) {
return groupData
}
}
export const getGroupNames = async (listOfGroups) => {
let groups = [];
if (listOfGroups && Array.isArray(listOfGroups)) {
for (const group of listOfGroups) {
const groupInfo = await getGroupInfo(group.groupId);
if (groupInfo) {
groups.push({ ...group, ...groupInfo });
}
}
}
return groups;
}
export const UserListOfInvites = ({myAddress, setInfoSnack, setOpenSnack}) => {
const {txList, setTxList, show} = useContext(MyContext)
const [invites, setInvites] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to
const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open
const listRef = useRef();
const getRequests = async () => {
try {
const response = await fetch(`${getBaseApiReact()}/groups/invites/${myAddress}/?limit=0`);
const inviteData = await response.json();
const resMoreData = await getGroupNames(inviteData)
setInvites(resMoreData);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
getRequests();
}, []);
const handlePopoverOpen = (event, index) => {
setPopoverAnchor(event.currentTarget);
setOpenPopoverIndex(index);
};
const handlePopoverClose = () => {
setPopoverAnchor(null);
setOpenPopoverIndex(null);
};
const handleJoinGroup = async (groupId, groupName)=> {
try {
const fee = await getFee('JOIN_GROUP')
await show({
message: "Would you like to perform an JOIN_GROUP transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoading(true);
await new Promise((res, rej)=> {
chrome.runtime.sendMessage({ action: "joinGroup", payload: {
groupId,
}}, (response) => {
if (!response?.error) {
setTxList((prev)=> [{
...response,
type: 'joined-group',
label: `Joined Group ${groupName}: awaiting confirmation`,
labelDone: `Joined Group ${groupName}: success !`,
done: false,
groupId,
}, ...prev])
res(response)
setInfoSnack({
type: "success",
message: "Successfully requested to join group. It may take a couple of minutes for the changes to propagate",
});
setOpenSnack(true);
handlePopoverClose();
return
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error)
});
})
} catch (error) {
} finally {
setIsLoading(false);
}
}
const rowRenderer = ({ index, key, parent, style }) => {
const invite = invites[index];
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
{({ measure }) => (
<div style={style} onLoad={measure}>
<ListItem disablePadding>
<Popover
open={openPopoverIndex === index}
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
style={{ marginTop: "8px" }}
>
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<Typography>Join {invite?.groupName}</Typography>
<LoadingButton
loading={isLoading}
loadingPosition="start"
variant="contained" onClick={()=> handleJoinGroup(invite?.groupId, invite?.groupName)}>Join group</LoadingButton>
</Box>
</Popover>
<ListItemButton onClick={(event) => handlePopoverOpen(event, index)}>
<ListItemText primary={invite?.groupName} secondary={invite?.description} />
</ListItemButton>
</ListItem>
</div>
)}
</CellMeasurer>
);
};
return (
<div>
<p>Invite list</p>
<div style={{ position: 'relative', height: '500px', width: '600px', display: 'flex', flexDirection: 'column', flexShrink: 1 }}>
<AutoSizer>
{({ height, width }) => (
<List
ref={listRef}
width={width}
height={height}
rowCount={invites.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
deferredMeasurementCache={cache}
/>
)}
</AutoSizer>
</div>
</div>
);
}

View File

@@ -0,0 +1,109 @@
import React, { useEffect, useRef } from 'react';
import { getBaseApiReactSocket } from '../../App';
export const WebSocketActive = ({ myAddress }) => {
const socketRef = useRef(null); // WebSocket reference
const timeoutIdRef = useRef(null); // Timeout ID reference
const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference
const forceCloseWebSocket = () => {
if (socketRef.current) {
console.log('Force closing the WebSocket');
clearTimeout(timeoutIdRef.current);
clearTimeout(groupSocketTimeoutRef.current);
socketRef.current.close(1000, 'forced');
socketRef.current = null;
}
};
useEffect(() => {
if (!myAddress) return; // Only proceed if myAddress is set
if (!window?.location?.href?.includes("?main=true")) return;
const pingHeads = () => {
try {
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send('ping');
timeoutIdRef.current = setTimeout(() => {
if (socketRef.current) {
socketRef.current.close();
clearTimeout(groupSocketTimeoutRef.current);
}
}, 5000); // Close if no pong in 5 seconds
}
} catch (error) {
console.error('Error during ping:', error);
}
};
const initWebsocketMessageGroup = async () => {
forceCloseWebSocket(); // Ensure we close any existing connection
const currentAddress = myAddress;
try {
const socketLink = `${getBaseApiReactSocket()}/websockets/chat/active/${currentAddress}?encoding=BASE64`;
socketRef.current = new WebSocket(socketLink);
socketRef.current.onopen = () => {
console.log('WebSocket connection opened');
setTimeout(pingHeads, 50); // Initial ping
};
socketRef.current.onmessage = (e) => {
try {
if (e.data === 'pong') {
clearTimeout(timeoutIdRef.current);
groupSocketTimeoutRef.current = setTimeout(pingHeads, 45000); // Ping every 45 seconds
} else {
const data = JSON.parse(e.data);
const filteredGroups = data.groups?.filter(item => item?.groupId !== 0) || [];
const sortedGroups = filteredGroups.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
const sortedDirects = (data?.direct || []).filter(item =>
item?.name !== 'extension-proxy' && item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH'
).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
chrome.runtime.sendMessage({
action: 'handleActiveGroupDataFromSocket',
payload: {
groups: sortedGroups,
directs: sortedDirects,
},
});
}
} catch (error) {
console.error('Error parsing onmessage data:', error);
}
};
socketRef.current.onclose = (event) => {
clearTimeout(groupSocketTimeoutRef.current);
clearTimeout(timeoutIdRef.current);
console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`);
if (event.reason !== 'forced' && event.code !== 1000) {
setTimeout(() => initWebsocketMessageGroup(), 10000); // Retry after 10 seconds
}
};
socketRef.current.onerror = (error) => {
console.error('WebSocket error:', error);
clearTimeout(groupSocketTimeoutRef.current);
clearTimeout(timeoutIdRef.current);
if (socketRef.current) {
socketRef.current.close();
}
};
} catch (error) {
console.error('Error initializing WebSocket:', error);
}
};
initWebsocketMessageGroup(); // Initialize WebSocket on component mount
return () => {
forceCloseWebSocket(); // Clean up WebSocket on component unmount
};
}, [myAddress]);
return null;
};