Publish Forum button changed to Manage Forums. It handles both publishing and editing forums.

This commit is contained in:
Qortal Dev 2024-10-02 09:45:23 -06:00
parent 0659433f03
commit dd500a3ebb
14 changed files with 512 additions and 335 deletions

View File

@ -1,31 +1,20 @@
// @ts-nocheck // @ts-nocheck
import { signal } from "@preact/signals-react"; import { CssBaseline } from "@mui/material";
import { useEffect } from "react";
import { Routes, Route } from "react-router-dom";
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from "@mui/material/styles";
import { CssBaseline } from "@mui/material";
import { ForumData } from "./pages/Forum/ForumModal";
import { Home } from "./pages/Home/Home";
import { lightTheme, darkTheme } from "./styles/theme";
import { store } from "./state/store";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { fetchForumData } from "./utils/QortalRequests"; import { Route, Routes } from "react-router-dom";
import GlobalWrapper from "./wrappers/GlobalWrapper";
import DownloadWrapper from "./wrappers/DownloadWrapper";
import Notification from "./components/common/Notification/Notification"; import Notification from "./components/common/Notification/Notification";
import { Home } from "./pages/Home/Home";
export const forums = signal<ForumData[]>([]); import { store } from "./state/store";
import { darkTheme } from "./styles/theme";
import DownloadWrapper from "./wrappers/DownloadWrapper";
import GlobalWrapper from "./wrappers/GlobalWrapper";
function App() { function App() {
const themeColor = window._qdnTheme; const themeColor = window._qdnTheme;
useEffect(() => {
fetchForumData().then(data => {
if (data) forums.value = data;
console.log("forums is : ", forums.value);
});
}, []);
return ( return (
<Provider store={store}> <Provider store={store}>
<ThemeProvider theme={darkTheme}> <ThemeProvider theme={darkTheme}>
@ -35,6 +24,7 @@ function App() {
<CssBaseline /> <CssBaseline />
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/forum/:" element={<Home />} />
<Route path="/sponsorshipData" element={<Home />} /> <Route path="/sponsorshipData" element={<Home />} />
</Routes> </Routes>
</GlobalWrapper> </GlobalWrapper>

View File

@ -4,28 +4,38 @@ import {
MenuItem, MenuItem,
OutlinedInput, OutlinedInput,
Select, Select,
SelectChangeEvent,
} from "@mui/material"; } from "@mui/material";
import { SxProps } from "@mui/system"; import { SxProps } from "@mui/system";
import { signal, Signal } from "@preact/signals-react"; import { signal, Signal, useSignal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime"; import { useSignals } from "@preact/signals-react/runtime";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
interface SelectFieldProps { interface SelectFieldProps {
options: string[]; options: string[];
value: Signal<string>; initialValue?: string;
label: string; label: string;
sx?: SxProps; sx?: SxProps;
afterChange?: (e: string) => void;
width?: string;
} }
export const SelectField = ({ export const SelectField = ({
options, options,
value, initialValue = "",
label, label,
sx = {}, sx = {},
width = "100%",
afterChange,
}: SelectFieldProps) => { }: SelectFieldProps) => {
useSignals(); useSignals();
const value = useSignal<string>(initialValue);
useEffect(() => {
value.value = initialValue;
}, [initialValue]);
return ( return (
<FormControl fullWidth> <FormControl fullWidth sx={{ width: width }}>
<InputLabel <InputLabel
sx={{ sx={{
fontSize: "100%", fontSize: "100%",
@ -41,7 +51,9 @@ export const SelectField = ({
value={value.value || ""} value={value.value || ""}
variant={"standard"} variant={"standard"}
onChange={e => { onChange={e => {
value.value = e.target.value; const eventValue = e.target.value;
value.value = eventValue;
if (afterChange) afterChange(eventValue);
}} }}
sx={{ color: "black", ...sx }} sx={{ color: "black", ...sx }}
> >

View File

@ -1,28 +0,0 @@
import MenuBookIcon from "@mui/icons-material/MenuBook";
import React from "react";
import { useNavigate } from "react-router-dom";
import { forums } from "../../App";
import {
ComposeContainer,
ComposeP,
InstanceContainer,
} from "../Home/Home-styles";
import { ForumModal } from "./ForumModal";
export const ActionBar = () => {
const navigate = useNavigate();
return (
<InstanceContainer>
<ForumModal />
<ForumModal forumData={forums.value} />
<ComposeContainer
sx={{ width: "200px", marginLeft: "auto" }}
onClick={() => navigate("/sponsorshipData")}
>
<MenuBookIcon />
<ComposeP sx={{ fontSize: "70%" }}>{"Sponsorship Data"}</ComposeP>
</ComposeContainer>
</InstanceContainer>
);
};

View File

@ -0,0 +1,49 @@
import MenuBookIcon from "@mui/icons-material/MenuBook";
import { Signal, signal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { fetchForumData } from "../../../utils/QortalRequests";
import {
ComposeContainer,
ComposeP,
InstanceContainer,
} from "../../Home/Home-styles";
import { ForumData, ForumModal } from "../ForumModal";
export const forums = signal<ForumData[]>([]);
export const resetForumIndexes = (forums: Signal<ForumData[]>) => {
forums.value = forums.value.map((forum, index) => {
forum.listIndex = index;
return forum;
});
};
export const ActionBar = () => {
useSignals();
useEffect(() => {
fetchForumData().then(data => {
if (data)
forums.value = data.map((forum, index) => {
forum.listIndex = index;
return forum;
});
// console.log("forums are : ", forums.value);
});
}, []);
const navigate = useNavigate();
return (
<InstanceContainer>
<ForumModal forumData={forums} />
<ComposeContainer
sx={{ width: "200px", marginLeft: "auto" }}
onClick={() => navigate("/sponsorshipData")}
>
<MenuBookIcon />
<ComposeP sx={{ fontSize: "70%" }}>{"Sponsorship Data"}</ComposeP>
</ComposeContainer>
</InstanceContainer>
);
};

View File

@ -0,0 +1,121 @@
import { Box } from "@mui/material";
import { useSignal } from "@preact/signals-react";
import React, { PropsWithChildren } from "react";
import ComposeIconSVG from "../../../assets/svgs/ComposeIcon.svg";
import { CreateThreadIcon } from "../../../assets/svgs/CreateThreadIcon";
import ModalCloseSVG from "../../../assets/svgs/ModalClose.svg";
import { ReusableModal } from "../../../components/modals/ReusableModal";
import {
CloseContainer,
ComposeContainer,
ComposeIcon,
ComposeP,
InstanceFooter,
InstanceListContainer,
InstanceListHeader,
NewMessageCloseImg,
NewMessageHeaderP,
NewMessageSendButton,
NewMessageSendP,
} from "../../Home/Home-styles";
import { publishForum } from "../ForumModal-Data";
export interface ModalButtonProps {
onSubmit: () => Promise<boolean>;
onClose?: () => void;
isRenderModal: boolean;
modalLabel: string;
buttonLabel: string;
}
export const ModalButton = ({
onClose,
onSubmit,
isRenderModal,
modalLabel,
buttonLabel,
children,
}: PropsWithChildren<ModalButtonProps>) => {
const isOpen = useSignal<boolean>(false);
const closeModal = () => {
isOpen.value = false;
if (onClose) onClose();
};
return (
<Box
sx={{
display: "flex",
}}
>
{isRenderModal && (
<ComposeContainer onClick={e => (isOpen.value = true)}>
<ComposeIcon src={ComposeIconSVG} />
<ComposeP sx={{ fontSize: "70%" }}>{modalLabel}</ComposeP>
</ComposeContainer>
)}
<ReusableModal
open={isOpen.value}
customStyles={{
maxHeight: "95vh",
maxWidth: "950px",
height: "700px",
borderRadius: "12px 12px 0px 0px",
background: "var(--Mail-Background, #313338)",
padding: "0px",
gap: "0px",
}}
>
<InstanceListHeader
sx={{
backgroundColor: "unset",
height: "50px",
padding: "20px 42px",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<NewMessageHeaderP>{modalLabel}</NewMessageHeaderP>
<CloseContainer onClick={closeModal}>
<NewMessageCloseImg src={ModalCloseSVG} />
</CloseContainer>
</InstanceListHeader>
<InstanceListContainer
sx={{
backgroundColor: "rgba(217, 217, 217, 1)",
padding: "20px 42px",
height: "calc(100% - 150px)",
flexShrink: 0,
}}
>
{children}
</InstanceListContainer>
<InstanceFooter
sx={{
backgroundColor: "rgba(217, 217, 217, 1)",
padding: "20px 42px",
alignItems: "center",
height: "90px",
}}
>
<NewMessageSendButton
onClick={() => {
onSubmit().then(success => {
if (success) closeModal();
});
}}
>
<NewMessageSendP>{buttonLabel}</NewMessageSendP>
<CreateThreadIcon
color="red"
opacity={1}
height="25px"
width="25px"
/>
</NewMessageSendButton>
</InstanceFooter>
</ReusableModal>
</Box>
);
};

View File

@ -1,27 +1,33 @@
import { Input } from "@mui/material"; import { Input } from "@mui/material";
import { SxProps } from "@mui/system"; import { SxProps } from "@mui/system";
import { Signal, useSignalEffect } from "@preact/signals-react"; import { Signal, useSignal, useSignalEffect } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime"; import { useSignals } from "@preact/signals-react/runtime";
import React from "react"; import React, { useEffect } from "react";
interface QmailTextFieldProps { interface QmailTextFieldProps {
value: Signal<string>;
label: string; label: string;
sx?: SxProps; sx?: SxProps;
filter?: RegExp; filter?: RegExp;
maxLength?: number; maxLength?: number;
initialValue?: string;
afterChange?: (s: string) => void;
} }
export const QmailTextField = ({ export const QmailTextField = ({
value,
label, label,
sx = {}, sx = {},
filter, filter,
maxLength = 60, maxLength = 60,
initialValue = "",
afterChange,
}: QmailTextFieldProps) => { }: QmailTextFieldProps) => {
useSignals(); useSignals();
const value = useSignal<string>(initialValue);
useSignalEffect(() => { useSignalEffect(() => {
if (filter) value.value = value.value.replace(filter, ""); if (filter) value.value = value.value.replace(filter, "");
}); });
useEffect(() => {
value.value = initialValue;
}, [initialValue]);
return ( return (
<> <>
@ -29,7 +35,9 @@ export const QmailTextField = ({
id="standard-adornment-name" id="standard-adornment-name"
value={value.value} value={value.value}
onChange={e => { onChange={e => {
if (e.target.value.length <= maxLength) value.value = e.target.value; const eventValue = e.target.value;
if (eventValue.length <= maxLength) value.value = eventValue;
if (afterChange) afterChange(value.value);
}} }}
placeholder={label} placeholder={label}
disableUnderline disableUnderline

View File

@ -21,13 +21,13 @@ export const Forum = ({
}: ForumData) => { }: ForumData) => {
const [currentThread, setCurrentThread] = useState<any>(null); const [currentThread, setCurrentThread] = useState<any>(null);
const openNewThread = () => { // const openNewThread = () => {
if (currentThread) { // if (currentThread) {
executeEvent("openNewThreadMessageModal", {}); // executeEvent("openNewThreadMessageModal", {});
return; // return;
} // }
executeEvent("openNewThreadModal", {}); // executeEvent("openNewThreadModal", {});
}; // };
const forumWidth = 95; const forumWidth = 95;
const forumMarginLeft = (100 - forumWidth) / 2; const forumMarginLeft = (100 - forumWidth) / 2;
@ -51,8 +51,8 @@ export const Forum = ({
}} }}
> >
<span>{title}</span> <span>{title}</span>
<Box>{descriptionText.trim()}</Box>
</Box> </Box>
<Box>{descriptionText.trim()}</Box>
</Box> </Box>
// <Box> // <Box>

View File

@ -1,30 +1,18 @@
import { ReadonlySignal } from "@preact/signals-react"; import { FORUMS_ID } from "../../constants/Identifiers";
import ShortUniqueId from "short-unique-id";
import { forums } from "../../App";
import {
ATTATCHMENT_BASE,
FORUMS_ID,
THREAD_BASE,
} from "../../constants/Identifiers";
import {
MAIL_ATTACHMENT_SERVICE_TYPE,
MAIL_SERVICE_TYPE,
THREAD_SERVICE_TYPE,
} from "../../constants/mail";
import { appOwner } from "../../constants/Misc";
import { setNotification } from "../../state/features/notificationsSlice"; import { setNotification } from "../../state/features/notificationsSlice";
import { store } from "../../state/store"; import { store } from "../../state/store";
import { getGroup } from "../../utils/QortalRequests"; import { getGroup } from "../../utils/QortalRequests";
import { objectToBase64, toBase64 } from "../../utils/toBase64"; import { objectToBase64 } from "../../utils/toBase64";
import { ForumData } from "./ForumModal";
import { Group } from "./GroupPermissionsForm"; import { Group } from "./GroupPermissionsForm";
import { descriptionMaxLength, ForumData } from "./ForumModal";
const getGroupNames = async (groups: Group[]) => { const getGroupsData = async (groups: Group[]) => {
const groupPromises = groups.map(group => getGroup(group.id.value)); const groupPromises = groups.map(group => getGroup(group.id));
return await Promise.all(groupPromises); return await Promise.all(groupPromises);
}; };
const verifyData = async (formData: ForumData) => { const verifyData = async (formData: ForumData) => {
debugger;
const userName = store.getState()?.auth?.user?.name; const userName = store.getState()?.auth?.user?.name;
const { title, encryption, groups, descriptionHTML, descriptionText } = const { title, encryption, groups, descriptionHTML, descriptionText } =
formData; formData;
@ -38,51 +26,33 @@ const verifyData = async (formData: ForumData) => {
if (groups.filter(group => !!group.permissions).length < groups.length) if (groups.filter(group => !!group.permissions).length < groups.length)
errorMsg = "A group has empty permissions"; errorMsg = "A group has empty permissions";
const groupsWithNames = await getGroupNames(groups); const groupsWithNames = await getGroupsData(groups);
if ( if (
groupsWithNames.filter(group => !!group?.groupName).length < groups.length groupsWithNames.filter(group => !!group?.groupName).length < groups.length
) )
errorMsg = "A group ID provided doesn't exist"; errorMsg = "A group ID provided doesn't exist";
if (errorMsg) {
store.dispatch(
setNotification({
msg: errorMsg,
alertType: "error",
})
);
}
return errorMsg; return errorMsg;
}; };
export const addForum = async (formData: ForumData) => { const verifyAllData = async (formData: ForumData[]) => {
const errorMsg = await verifyData(formData); const errorListAll = await Promise.all(
if (errorMsg) return; formData.map(data => verifyData(data))
forums.value = [...forums.value, formData]; );
// don't loop through all errors, take the first one and use that.
const firstErrorIndex = errorListAll.findIndex(data => !!data);
if (firstErrorIndex === -1) return "";
return `Forum ${firstErrorIndex + 1} has error: ${
errorListAll[firstErrorIndex]
}`;
}; };
export const editForums = async (newForums: ForumData[]) => { export const publishForum = async (formData: ForumData[]): Promise<boolean> => {
const errorMsgPromises = newForums.map(forumData => verifyData(forumData));
const errorMsgs = await Promise.all(errorMsgPromises);
const errorMsgNum = errorMsgs.filter(msg => !!msg).length;
if (errorMsgNum > 0) return;
forums.value = newForums;
};
export const publishForum = async (formData: ForumData) => {
let success = false;
const errorMsg = await verifyData(formData);
if (errorMsg) return success;
try { try {
await addForum(formData); console.log("formData is: ", formData);
const errorMsg = await verifyAllData(formData);
const publishDescription = if (errorMsg) throw new Error(errorMsg);
formData.title +
"_" +
formData.descriptionText.substring(0, descriptionMaxLength);
const userName = store.getState()?.auth?.user?.name; const userName = store.getState()?.auth?.user?.name;
await qortalRequest({ await qortalRequest({
@ -90,40 +60,51 @@ export const publishForum = async (formData: ForumData) => {
name: userName, name: userName,
service: "METADATA", service: "METADATA",
identifier: FORUMS_ID, identifier: FORUMS_ID,
description: publishDescription, data64: await objectToBase64(formData),
data64: await objectToBase64(forums),
}); });
success = true;
store.dispatch( store.dispatch(
setNotification({ setNotification({
msg: "Forum published", msg: "Forum published",
alertType: "success", alertType: "success",
}) })
); );
return true;
} catch (error: any) { } catch (error: any) {
let notificationObj = null; console.log("error is: ", error);
const defaultErrorMessage = "Failed to submit forum data"; formErrorHandler(error);
// throw new Error(defaultErrorMessage);
if (typeof error === "string") { return false;
notificationObj = {
msg: error || defaultErrorMessage,
alertType: "error",
};
} else if (typeof error?.error === "string") {
notificationObj = {
msg: error?.error || defaultErrorMessage,
alertType: "error",
};
} else {
notificationObj = {
msg: error?.message || defaultErrorMessage,
alertType: "error",
};
}
if (!notificationObj) return;
store.dispatch(setNotification(notificationObj));
throw new Error(defaultErrorMessage);
} }
return success; };
const formErrorHandler = (error: any) => {
let notificationObj = null;
const defaultErrorMessage = "Failed to submit forum data";
if (typeof error === "string") {
notificationObj = {
msg: error || defaultErrorMessage,
alertType: "error",
};
} else if (typeof error?.error === "string") {
notificationObj = {
msg: error?.error || defaultErrorMessage,
alertType: "error",
};
} else {
notificationObj = {
msg: error?.message || defaultErrorMessage,
alertType: "error",
};
}
if (notificationObj) store.dispatch(setNotification(notificationObj));
};
export const forumToString = (forum: ForumData) =>
`${forum?.listIndex !== undefined ? forum.listIndex + 1 : "?"} - ${
forum?.title
}`;
export const deepCopyArray = (array: object[]) => {
return JSON.parse(JSON.stringify(array));
}; };

View File

@ -1,45 +1,25 @@
import MenuBookIcon from "@mui/icons-material/MenuBook"; import { Box, Button } from "@mui/material";
import { Box } from "@mui/material";
import { import {
ReadonlySignal,
Signal, Signal,
signal,
useComputed, useComputed,
useSignal, useSignal,
useSignalEffect,
} from "@preact/signals-react"; } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime"; import { useSignals } from "@preact/signals-react/runtime";
import React from "react"; import React, { useEffect } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import ComposeIconSVG from "../../assets/svgs/ComposeIcon.svg";
import { CreateThreadIcon } from "../../assets/svgs/CreateThreadIcon";
import ModalCloseSVG from "../../assets/svgs/ModalClose.svg";
import { SelectField } from "../../components/common/SelectField"; import { SelectField } from "../../components/common/SelectField";
import { Spacer } from "../../components/common/Spacer"; import { Spacer } from "../../components/common/Spacer";
import { TextEditor } from "../../components/common/TextEditor/TextEditor"; import { TextEditor } from "../../components/common/TextEditor/TextEditor";
import { ReusableModal } from "../../components/modals/ReusableModal";
import { useTestIdentifiers } from "../../constants/Identifiers"; import { useTestIdentifiers } from "../../constants/Identifiers";
import { appOwner } from "../../constants/Misc"; import { appOwner } from "../../constants/Misc";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import { import { NewMessageInputRow } from "../Home/Home-styles";
CloseContainer, import { resetForumIndexes } from "./Components/ActionBar";
ComposeContainer, import { ModalButton } from "./Components/ModalButton";
ComposeIcon, import { QmailTextField } from "./Components/QmailTextField";
ComposeP, import { deepCopyArray, forumToString, publishForum } from "./ForumModal-Data";
InstanceContainer,
InstanceFooter,
InstanceListContainer,
InstanceListHeader,
NewMessageCloseImg,
NewMessageHeaderP,
NewMessageInputRow,
NewMessageSendButton,
NewMessageSendP,
} from "../Home/Home-styles";
import { Group, GroupPermissionsForm } from "./GroupPermissionsForm"; import { Group, GroupPermissionsForm } from "./GroupPermissionsForm";
import { publishForum } from "./ForumModal-Data";
import { QmailTextField } from "./QmailTextField";
export type EncryptionType = "None" | "Group" | "GroupAdmin" | ""; export type EncryptionType = "None" | "Group" | "GroupAdmin" | "";
export interface ForumData { export interface ForumData {
@ -48,6 +28,7 @@ export interface ForumData {
groups: Group[]; groups: Group[];
descriptionHTML: string; descriptionHTML: string;
descriptionText: string; descriptionText: string;
listIndex: number;
} }
export type GroupPermissionType = "Read" | "Write"; export type GroupPermissionType = "Read" | "Write";
@ -60,159 +41,181 @@ export const titleMaxLength = 60;
export const descriptionMaxLength = 160; export const descriptionMaxLength = 160;
interface NewForumModalProps { interface NewForumModalProps {
forumData?: ForumData[]; forumData: Signal<ForumData[]>;
} }
export const ForumModal = ({ forumData }: NewForumModalProps) => { export const ForumModal = ({ forumData }: NewForumModalProps) => {
useSignals(); useSignals();
const isOpen = useSignal<boolean>(false);
const forumTitle = useSignal<string>(""); const tempData = useSignal<ForumData[]>([]);
const descriptionHTML = useSignal<string>("");
const descriptionText = useSignal<string>("");
const groups = useSignal<Group[]>([
{
id: signal<string>(""),
permissions: signal<GroupPermissionType>("Read"),
},
]);
const selectedEncryptionType = useSignal<EncryptionType | "">("None"); const emptyForum: ForumData = {
title: "",
const { user } = useSelector((state: RootState) => state.auth); encryption: "None",
groups: [],
const formData: ReadonlySignal<ForumData> = useComputed(() => { descriptionHTML: "",
return { descriptionText: "",
title: forumTitle.value, listIndex: 0,
encryption: selectedEncryptionType.value, };
groups: groups.value,
descriptionHTML: descriptionHTML.value, const { user } = useSelector((state: RootState) => state.auth);
descriptionText: descriptionText.value, const isRenderModal = user?.name === appOwner || useTestIdentifiers;
};
}); const selectedForumIndex = useSignal<number>(0);
const selectedForum = useComputed<ForumData>(() => {
const closeModal = () => { return tempData.value[selectedForumIndex.value];
isOpen.value = false; });
const initializeTempData = () => {
tempData.value =
forumData.value.length === 0
? [emptyForum]
: deepCopyArray(forumData.value);
selectedForumIndex.value = 0;
};
useEffect(() => {
initializeTempData();
}, [forumData.value]);
const addForum = () => {
const newForum = { ...emptyForum };
newForum.listIndex = tempData.value.length;
tempData.value = [...tempData.value, newForum];
selectedForumIndex.value = newForum.listIndex;
};
const removeForum = () => {
if (tempData.value.length <= 1) {
tempData.value = [{ ...emptyForum }];
selectedForumIndex.value = 0;
} else {
tempData.value = tempData.value.filter(
(group, index) => index !== selectedForumIndex.value
);
if (selectedForumIndex.value > 0) selectedForumIndex.value -= 1;
else selectedForumIndex.value = 0;
}
resetForumIndexes(tempData);
};
const updateForums = () => {
tempData.value = [...tempData.value];
}; };
const forumText = !!forumData ? "Edit Forums" : "Add Forum";
const publishText = !!forumData ? "Edit Forums" : "Create Forum";
return ( return (
<Box <ModalButton
sx={{ isRenderModal={isRenderModal}
display: "flex", onClose={() => initializeTempData()}
onSubmit={async () => {
updateForums();
const pub = await publishForum(tempData.value);
if (pub) forumData.value = tempData.value;
return pub;
}} }}
modalLabel={"Manage Forums"}
buttonLabel={"Publish Forums"}
> >
{(user?.name === appOwner || useTestIdentifiers) && ( <Box
<ComposeContainer onClick={e => (isOpen.value = true)}> sx={{
<ComposeIcon src={ComposeIconSVG} /> display: "flex",
<ComposeP sx={{ fontSize: "70%" }}>{forumText}</ComposeP> width: "100%",
</ComposeContainer> alignItems: "center",
)} gap: "10px",
<ReusableModal marginBottom: "10px",
open={isOpen.value}
customStyles={{
maxHeight: "95vh",
maxWidth: "950px",
height: "700px",
borderRadius: "12px 12px 0px 0px",
background: "var(--Mail-Background, #313338)",
padding: "0px",
gap: "0px",
}} }}
> >
<InstanceListHeader <SelectField
sx={{ options={tempData.value.map((forum, index) => forumToString(forum))}
backgroundColor: "unset", label={"Forum #"}
height: "50px", initialValue={forumToString(selectedForum.value)}
padding: "20px 42px", afterChange={s => {
flexDirection: "row", //tempData.value[selectedForumIndex.value] = selectedForum.value;
justifyContent: "space-between", tempData.value = [...tempData.value];
alignItems: "center", selectedForumIndex.value = +s.split(" ")[0] - 1;
}} }}
>
<NewMessageHeaderP>{forumText}</NewMessageHeaderP>
<CloseContainer onClick={closeModal}>
<NewMessageCloseImg src={ModalCloseSVG} />
</CloseContainer>
</InstanceListHeader>
<InstanceListContainer
sx={{ sx={{
backgroundColor: "rgba(217, 217, 217, 1)", "& .MuiSvgIcon-root": {
padding: "20px 42px", color: "black",
height: "calc(100% - 150px)", },
flexShrink: 0,
}} }}
width={"50%"}
/>
<Button
variant={"contained"}
sx={{ width: "25%", height: "35px", color: "white" }}
color={"success"}
onClick={addForum}
> >
<NewMessageInputRow sx={{ height: "80px", alignItems: "end" }}> Add Forum
<QmailTextField </Button>
value={forumTitle} <Button
label={"Forum Title"} variant={"contained"}
sx={{ sx={{ width: "25%", height: "35px", color: "white" }}
height: "60px", color={"error"}
borderBottom: "1px solid gray", onClick={removeForum}
}} >
maxLength={titleMaxLength} Remove Selected Forum
/> </Button>
<SelectField </Box>
options={["None", "Group", "GroupAdmin"]} <NewMessageInputRow sx={{ height: "80px", alignItems: "end" }}>
label={"Encryption Type"} <QmailTextField
value={selectedEncryptionType} initialValue={selectedForum.value?.title}
sx={{ afterChange={s => {
"& .MuiSvgIcon-root": { tempData.value[selectedForumIndex.value].title = s;
color: "gray", updateForums();
}, }}
}} label={"Forum Title"}
/>
</NewMessageInputRow>
<GroupPermissionsForm groups={groups} />
<Spacer height="40px" />
<Box
sx={{
maxHeight: "40vh",
}}
>
<p style={{ color: "black" }}> Description </p>
<TextEditor
inlineContent={descriptionHTML.value}
setInlineContent={(
value: any,
delta: any,
source: any,
editor: any
) => {
descriptionHTML.value = value;
descriptionText.value = editor.getText(value);
}}
/>
</Box>
</InstanceListContainer>
<InstanceFooter
sx={{ sx={{
backgroundColor: "rgba(217, 217, 217, 1)", height: "60px",
padding: "20px 42px", borderBottom: "1px solid gray",
alignItems: "center",
height: "90px",
}} }}
> maxLength={titleMaxLength}
<NewMessageSendButton />
onClick={() => { <SelectField
publishForum(formData.value).then(success => { options={["None", "Group", "GroupAdmin"]}
if (success) closeModal(); label={"Encryption Type"}
}); initialValue={selectedForum.value?.encryption}
}} afterChange={s =>
> (selectedForum.value.encryption = s as EncryptionType)
<NewMessageSendP>{publishText}</NewMessageSendP> }
sx={{
<CreateThreadIcon "& .MuiSvgIcon-root": {
color="red" color: "gray",
opacity={1} height: "60px",
height="25px" },
width="25px" }}
/> />
</NewMessageSendButton> </NewMessageInputRow>
</InstanceFooter> <GroupPermissionsForm
</ReusableModal> initialGroups={selectedForum.value?.groups}
</Box> afterChange={g => {
tempData.value[selectedForumIndex.value].groups = g;
//updateForums();
}}
/>
<Spacer height="40px" />
<Box
sx={{
maxHeight: "40vh",
}}
>
<p style={{ color: "black" }}> Description </p>
<TextEditor
inlineContent={selectedForum.value?.descriptionHTML}
setInlineContent={(
value: any,
delta: any,
source: any,
editor: any
) => {
selectedForum.value.descriptionHTML = value;
selectedForum.value.descriptionText = editor.getText(value);
}}
/>
</Box>
</ModalButton>
); );
}; };

View File

@ -1,3 +1,27 @@
export const ForumThreads = () => { import { useSignal } from "@preact/signals-react";
return <></>; import { useSignals } from "@preact/signals-react/runtime";
import { useState } from "react";
import { GroupData } from "../../utils/QortalRequests";
import { GroupMail } from "../Mail/GroupMail";
import { Thread } from "../Mail/Thread";
export interface ForumThreadsProps {
id: number;
}
export const ForumThreads = ({ id }: ForumThreadsProps) => {
useSignals();
const [currentThread, setCurrentThread] = useState<any>(null);
const [filterMode, setFilterMode] = useState<string>("Recently active");
const groupInfo = useSignal<GroupData | undefined>(undefined);
return (
<GroupMail
groupInfo={groupInfo.value}
currentThread={currentThread}
setCurrentThread={setCurrentThread}
setFilterMode={setFilterMode}
filterMode={filterMode}
/>
);
}; };

View File

@ -1,41 +1,46 @@
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "@mui/icons-material/Remove"; import RemoveIcon from "@mui/icons-material/Remove";
import { import { Box, IconButton } from "@mui/material";
Box,
Button,
IconButton,
MenuItem,
OutlinedInput,
Select,
SelectChangeEvent,
} from "@mui/material";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { signal, Signal } from "@preact/signals-react"; import { Signal, useSignal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime"; import { useSignals } from "@preact/signals-react/runtime";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { SelectField } from "../../components/common/SelectField"; import { SelectField } from "../../components/common/SelectField";
import { NewMessageInputRow } from "../Home/Home-styles"; import { NewMessageInputRow } from "../Home/Home-styles";
import { QmailTextField } from "./Components/QmailTextField";
import { GroupPermissionType } from "./ForumModal"; import { GroupPermissionType } from "./ForumModal";
import { QmailTextField } from "./QmailTextField";
export interface Group { export interface Group {
id: Signal<string>; id: string;
permissions: Signal<GroupPermissionType>; permissions: GroupPermissionType;
} }
export interface GroupPermissionsFormProps { export interface GroupPermissionsFormProps {
groups: Signal<Group[]>; initialGroups?: Group[];
afterChange?: (g: Group[]) => void;
} }
export const GroupPermissionsForm = ({ groups }: GroupPermissionsFormProps) => { export const GroupPermissionsForm = ({
initialGroups,
afterChange,
}: GroupPermissionsFormProps) => {
useSignals(); useSignals();
const uid = new ShortUniqueId();
const newGroup = { const newGroup = {
id: signal<string>(""), id: "",
permissions: signal<GroupPermissionType>("Read"), permissions: "Read",
} as Group; } as Group;
const groups = useSignal<Group[]>(initialGroups || [newGroup]);
useEffect(() => {
if (initialGroups)
groups.value = initialGroups?.length > 0 ? initialGroups : [newGroup];
}, [initialGroups]);
const uid = new ShortUniqueId();
const addGroup = () => { const addGroup = () => {
groups.value = [...groups.value, newGroup]; groups.value = [...groups.value, newGroup];
if (afterChange) afterChange(groups.value);
}; };
const removeGroup = (groupIndex: number) => { const removeGroup = (groupIndex: number) => {
@ -44,8 +49,12 @@ export const GroupPermissionsForm = ({ groups }: GroupPermissionsFormProps) => {
(group, index) => index !== groupIndex (group, index) => index !== groupIndex
); );
else groups.value = [newGroup]; else groups.value = [newGroup];
if (afterChange) afterChange(groups.value);
}; };
const updateGroups = () => {
if (afterChange) afterChange(groups.value);
};
const buttonStyle = { const buttonStyle = {
width: "70px", width: "70px",
height: "70px", height: "70px",
@ -99,7 +108,11 @@ export const GroupPermissionsForm = ({ groups }: GroupPermissionsFormProps) => {
<RemoveIcon sx={{ ...iconStyle, fontSize: 50 }} /> <RemoveIcon sx={{ ...iconStyle, fontSize: 50 }} />
</StyledIconButton> </StyledIconButton>
<QmailTextField <QmailTextField
value={group.id} afterChange={s => {
group.id = s;
updateGroups();
}}
initialValue={group.id}
label={"Group ID"} label={"Group ID"}
filter={/[^0-9]/} filter={/[^0-9]/}
sx={{ sx={{
@ -110,10 +123,13 @@ export const GroupPermissionsForm = ({ groups }: GroupPermissionsFormProps) => {
maxLength={10} maxLength={10}
/> />
</Box> </Box>
<SelectField <SelectField
options={options} options={options}
value={group.permissions} initialValue={group.permissions}
afterChange={s => {
group.permissions = s as GroupPermissionType;
updateGroups();
}}
label={"Group Permissions"} label={"Group Permissions"}
sx={{ sx={{
"& .MuiSvgIcon-root": { "& .MuiSvgIcon-root": {

View File

@ -84,7 +84,7 @@ export const MailBodyInnerScroll = styled(Box)`
export const ComposeContainer = styled(Box)(({ theme }) => ({ export const ComposeContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
width: "150px", width: "100%",
alignItems: "center", alignItems: "center",
gap: "7px", gap: "7px",
height: "100%", height: "100%",

View File

@ -1,8 +1,7 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { useSignals } from "@preact/signals-react/runtime"; import { useSignals } from "@preact/signals-react/runtime";
import React from "react"; import React from "react";
import { forums } from "../../App"; import { ActionBar, forums } from "../Forum/Components/ActionBar";
import { ActionBar } from "../Forum/ActionBar";
import { Forum } from "../Forum/Forum"; import { Forum } from "../Forum/Forum";
export const Home = () => { export const Home = () => {

View File

@ -1,5 +1,6 @@
import { FORUMS_ID } from "../constants/Identifiers"; import { FORUMS_ID } from "../constants/Identifiers";
import { appOwner } from "../constants/Misc"; import { appOwner } from "../constants/Misc";
import { ForumData } from "../pages/Forum/ForumModal";
export interface GroupData { export interface GroupData {
groupId: number; groupId: number;
@ -19,6 +20,7 @@ export const listGroups = async () => {
}; };
export const getGroup = async (groupID: number | string) => { export const getGroup = async (groupID: number | string) => {
if (!groupID) return undefined;
const url = `/groups/${groupID.toString()}`; const url = `/groups/${groupID.toString()}`;
try { try {
@ -36,12 +38,12 @@ export const getGroup = async (groupID: number | string) => {
export const fetchForumData = async () => { export const fetchForumData = async () => {
try { try {
return await qortalRequest({ return (await qortalRequest({
action: "FETCH_QDN_RESOURCE", action: "FETCH_QDN_RESOURCE",
name: appOwner, name: appOwner,
service: "METADATA", service: "METADATA",
identifier: FORUMS_ID, identifier: FORUMS_ID,
}); })) as ForumData[];
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return undefined; return undefined;