diff --git a/package-lock.json b/package-lock.json index bd2923f..3f52dee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lemmy-modder", - "version": "1.3.7", + "version": "1.3.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lemmy-modder", - "version": "1.3.7", + "version": "1.3.8", "license": "MIT", "dependencies": { "@babel/core": "^7.23.2", diff --git a/package.json b/package.json index be861c7..ab02794 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-modder", - "version": "1.3.7", + "version": "1.3.8", "description": "Lemmy Moderation App", "author": "tgxn", "license": "MIT", diff --git a/src/components/Actions/CommentButtons.jsx b/src/components/Actions/CommentButtons.jsx index 2d769c5..46f2dce 100644 --- a/src/components/Actions/CommentButtons.jsx +++ b/src/components/Actions/CommentButtons.jsx @@ -9,7 +9,7 @@ import { useLemmyHttpAction } from "../../hooks/useLemmyHttp.js"; import { BaseActionButton, ActionConfirmButton, InputElement, ConfirmDialog } from "./BaseElements.jsx"; import { getSiteData } from "../../hooks/getSiteData"; -import { selectShowResolved } from "../../reducers/configReducer.js"; +import { selectShowResolved, selectMandatoryModComment } from "../../reducers/configReducer.js"; export const ResolveCommentReportButton = ({ report, ...props }) => { const queryClient = useQueryClient(); @@ -86,6 +86,8 @@ export const ResolveCommentReportButton = ({ report, ...props }) => { }; export const RemoveCommentButton = ({ report, ...props }) => { + const mandatoryModComment = useSelector(selectMandatoryModComment); + const [confirmOpen, setConfirmOpen] = React.useState(false); const [removeReason, setRemoveReason] = React.useState(""); @@ -139,6 +141,7 @@ export const RemoveCommentButton = ({ report, ...props }) => { // placeholder={`reason`} // inputText="Reason for removal" // isRequired={!report.comment.removed} + disabled={mandatoryModComment && removeReason == ""} buttonMessage={actionText} color={actionColor} onConfirm={() => { @@ -157,12 +160,14 @@ export const RemoveCommentButton = ({ report, ...props }) => { }; export const PurgeCommentButton = ({ report, ...props }) => { + const mandatoryModComment = useSelector(selectMandatoryModComment); + const [confirmOpen, setConfirmOpen] = React.useState(false); const [purgeReason, setPurgeReason] = React.useState(""); const queryClient = useQueryClient(); - const { data, callAction, isSuccess, isLoading, error } = useLemmyHttpAction("purgePost"); + const { data, callAction, isSuccess, isLoading, error } = useLemmyHttpAction("purgeComment"); React.useEffect(() => { if (isSuccess) { @@ -178,7 +183,7 @@ export const PurgeCommentButton = ({ report, ...props }) => { <> setConfirmOpen(true)} {...props} @@ -187,8 +192,8 @@ export const PurgeCommentButton = ({ report, ...props }) => { open={confirmOpen} loading={isLoading} error={error} - title={`Purge Post`} - message={`Are you sure you want to purge this post? Purging will delete this item, and all its children from the database. You will not be able to recover the data. Use with extreme caution.`} + title={`Purge Comment`} + message={`Are you sure you want to purge this comment? Purging will delete this item, and all its children from the database. You will not be able to recover the data. Use with extreme caution.`} extraElements={[ { />, ]} // disabled={purgeReason == ""} + disabled={mandatoryModComment && purgeReason == ""} buttonMessage={"Purge"} color={"danger"} onConfirm={() => { - callAction({ post_id: report.post.id, reason: purgeReason }); + callAction({ _id: report.comment.id, reason: purgeReason }); }} onCancel={() => { setConfirmOpen(false); diff --git a/src/components/Actions/GenButtons.jsx b/src/components/Actions/GenButtons.jsx index 0f01ffe..b0ea57f 100644 --- a/src/components/Actions/GenButtons.jsx +++ b/src/components/Actions/GenButtons.jsx @@ -1,5 +1,7 @@ import React from "react"; +import { useDispatch, useSelector } from "react-redux"; + import { useQueryClient } from "@tanstack/react-query"; import moment from "moment"; @@ -14,8 +16,12 @@ import { ConfirmDialog, } from "./BaseElements.jsx"; +import { selectMandatoryModComment } from "../../reducers/configReducer"; + // banFromCommunity export const BanUserCommunityButton = ({ person, community, isBanned, ...props }) => { + const mandatoryModComment = useSelector(selectMandatoryModComment); + const [confirmOpen, setConfirmOpen] = React.useState(false); const [banReason, setBanReason] = React.useState(""); const [removeData, setRemoveData] = React.useState(false); @@ -91,7 +97,7 @@ export const BanUserCommunityButton = ({ person, community, isBanned, ...props } /> ) : null, ]} - // disabled={banReason == ""} + disabled={mandatoryModComment && banReason == ""} buttonMessage={actionText} color={actionColor} onConfirm={() => { @@ -133,6 +139,8 @@ export const BanUserCommunityButton = ({ person, community, isBanned, ...props } // band from site export const BanUserSiteButton = ({ person, ...props }) => { + const mandatoryModComment = useSelector(selectMandatoryModComment); + const [confirmOpen, setConfirmOpen] = React.useState(false); const [banReason, setBanReason] = React.useState(""); const [removeData, setRemoveData] = React.useState(false); @@ -204,7 +212,7 @@ export const BanUserSiteButton = ({ person, ...props }) => { /> ) : null, ]} - // disabled={banReason == ""} + disabled={mandatoryModComment && banReason == ""} buttonMessage={actionText} color={actionColor} onConfirm={() => { @@ -245,6 +253,8 @@ export const BanUserSiteButton = ({ person, ...props }) => { // PURGE from site export const PurgeUserSiteButton = ({ person, ...props }) => { + const mandatoryModComment = useSelector(selectMandatoryModComment); + const [confirmOpen, setConfirmOpen] = React.useState(false); const [purgeReason, setPurgeReason] = React.useState(""); @@ -294,7 +304,7 @@ export const PurgeUserSiteButton = ({ person, ...props }) => { placeholder={`${actionText.toLowerCase()} reason`} />, ]} - // disabled={purgeReason == ""} + disabled={mandatoryModComment && purgeReason == ""} buttonMessage={actionText} color={actionColor} onConfirm={() => { diff --git a/src/components/Actions/PMButtons.jsx b/src/components/Actions/PMButtons.jsx index 7ae0cb8..1f17645 100644 --- a/src/components/Actions/PMButtons.jsx +++ b/src/components/Actions/PMButtons.jsx @@ -11,7 +11,7 @@ import { useLemmyHttpAction } from "../../hooks/useLemmyHttp.js"; import { BaseActionButton, ActionConfirmButton, InputElement, ConfirmDialog } from "./BaseElements.jsx"; import { getSiteData } from "../../hooks/getSiteData"; -import { selectShowResolved } from "../../reducers/configReducer.js"; +import { selectShowResolved, selectMandatoryModComment } from "../../reducers/configReducer.js"; // allow resolving / unresolving a post report // resolvePrivateMessageReport @@ -93,7 +93,10 @@ export const ResolvePMReportButton = ({ report, ...props }) => { }; // deletePrivateMessage +// onyl useful for owner export const DeletePMButton = ({ report, ...props }) => { + const mandatoryModComment = useSelector(selectMandatoryModComment); + const [confirmOpen, setConfirmOpen] = React.useState(false); const [removeReason, setRemoveReason] = React.useState(""); @@ -147,6 +150,7 @@ export const DeletePMButton = ({ report, ...props }) => { // placeholder={`reason`} // inputText="Reason for removal" // isRequired={!report.post.removed} + disabled={mandatoryModComment && removeReason == ""} buttonMessage={actionText} color={actionColor} onConfirm={() => { diff --git a/src/components/Actions/PostButtons.jsx b/src/components/Actions/PostButtons.jsx index 679fd1b..8fc40af 100644 --- a/src/components/Actions/PostButtons.jsx +++ b/src/components/Actions/PostButtons.jsx @@ -13,6 +13,8 @@ import { BaseActionButton, ActionConfirmButton, InputElement, ConfirmDialog } fr import { getSiteData } from "../../hooks/getSiteData"; import { selectShowResolved } from "../../reducers/configReducer.js"; +import { selectMandatoryModComment } from "../../reducers/configReducer"; + // allow resolving / unresolving a post report export const ResolvePostReportButton = ({ report, ...props }) => { const queryClient = useQueryClient(); @@ -140,6 +142,8 @@ export const DeletePostButton = ({ report, ...props }) => { // A moderator remove for a post. export const RemovePostButton = ({ report, ...props }) => { + const mandatoryModComment = useSelector(selectMandatoryModComment); + const [confirmOpen, setConfirmOpen] = React.useState(false); const [removeReason, setRemoveReason] = React.useState(""); @@ -193,6 +197,7 @@ export const RemovePostButton = ({ report, ...props }) => { // placeholder={`reason`} // inputText="Reason for removal" // isRequired={!report.post.removed} + disabled={mandatoryModComment && removeReason == ""} buttonMessage={actionText} color={actionColor} onConfirm={() => { @@ -208,6 +213,8 @@ export const RemovePostButton = ({ report, ...props }) => { // completely purge a post export const PurgePostButton = ({ report, ...props }) => { + const mandatoryModComment = useSelector(selectMandatoryModComment); + const [confirmOpen, setConfirmOpen] = React.useState(false); const [purgeReason, setPurgeReason] = React.useState(""); @@ -252,7 +259,7 @@ export const PurgePostButton = ({ report, ...props }) => { placeholder={`purge reason`} />, ]} - disabled={purgeReason == ""} + disabled={mandatoryModComment && purgeReason == ""} buttonMessage={"Purge"} color={"danger"} onConfirm={() => { diff --git a/src/components/Content/PostThumb.jsx b/src/components/Content/PostThumb.jsx index 6516a02..8e5335d 100644 --- a/src/components/Content/PostThumb.jsx +++ b/src/components/Content/PostThumb.jsx @@ -1,27 +1,18 @@ import React, { useState, useEffect, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import Tooltip from "@mui/joy/Tooltip"; import AspectRatio from "@mui/joy/AspectRatio"; import Typography from "@mui/joy/Typography"; - import Box from "@mui/joy/Box"; import Modal from "@mui/joy/Modal"; -import CircularProgress from "@mui/joy/CircularProgress"; - import LaunchIcon from "@mui/icons-material/Launch"; import { SanitizedLink } from "../Display.jsx"; import { Image, Video } from "./Image.jsx"; -import { - setConfigItem, - setConfigItemJson, - selectBlurNsfw, - selectShowAvatars, - selectNsfwWords, -} from "../../reducers/configReducer"; +import { selectBlurNsfw, selectNsfwWords } from "../../reducers/configReducer"; function ThumbWrapper({ width = 200, tooltip, modal = null, children }) { return ( diff --git a/src/components/Display.jsx b/src/components/Display.jsx index 474f410..9c13614 100644 --- a/src/components/Display.jsx +++ b/src/components/Display.jsx @@ -136,13 +136,14 @@ export const FediverseChipLink = ({ href, size = "md", ...props }) => { ); }; -export function UserAvatar({ source, ...props }) { +export function UserAvatar({ source, size, ...props }) { return ( { + const personRole = getUserRole(user); + console.log("personRole", personRole); - const [isLoading, setIsLoading] = React.useState(false); + let userIcon = RoleIcons[personRole](); + console.log("userIcon", userIcon); + return userIcon; + }, [user]); - const { baseUrl, siteData, localPerson, userRole } = getSiteData(); + return ( + { + // delete cache for current user + queryClient.invalidateQueries({ queryKey: ["lemmyHttp", localPerson.id] }); + dispatch(logoutCurrent()); + + dispatch(setAccountIsLoading(true)); + + try { + const lemmyClient = new LemmyHttp(`https://${user.base}`); + + const getSite = await lemmyClient.getSite({ + auth: user.jwt, + }); + + // there must be a user returned in this api call + if (!getSite.my_user) { + throw new Error("jwt does not provide auth, re-authenticate"); + } + + // TODO we need to update the user's details in the saved accounts array too, if this is a saved session + dispatch(setCurrentUser(user.base, user.jwt, getSite)); + } catch (e) { + toast(typeof e == "string" ? e : e.message); + } finally { + dispatch(setAccountIsLoading(false)); + } + }} + > + + + + + {user.site.my_user?.local_user_view?.person.name}@{user.base} + + {roleIcon} + + ); +} + +export default function AccountMenu() { + const users = useSelector(selectUsers); + + const { localUser, localPerson, userRole } = getSiteData(); let userTooltip = "user"; - let userIcon = ; + let userIcon = RoleIcons[userTooltip](); if (userRole == "admin") { userTooltip = "admin"; userIcon = ; @@ -79,82 +116,42 @@ export default function AccountMenu({ anchorEl, open, onClose }) { const parsedActor = parseActorId(localPerson.actor_id); return ( - - {users && users.length > 0 && ( - <> - {users.map((user, index) => { - return ( - { - onClose(); - - queryClient.invalidateQueries({ queryKey: ["lemmyHttp", localPerson.id] }); - dispatch(logoutCurrent()); - - setIsLoading(true); - - dispatch(setAccountIsLoading(true)); - - try { - const lemmyClient = new LemmyHttp(`https://${user.base}`); - - const getSite = await lemmyClient.getSite({ - auth: user.jwt, - }); - - if (!getSite.my_user) { - // set instance base to the current instance - // setInstanceBase(user.base); - // setUsername(user.site.my_user.local_user_view?.person.name); - - throw new Error("jwt does not provide auth, re-authenticate"); - } - - // if (saveSession) { - // dispatch(addUser(user.base, auth.jwt, getSite)); - // } else { - // dispatch(setCurrentUser(user.base, auth.jwt, getSite)); - dispatch(setCurrentUser(user.base, user.jwt, getSite)); - // } - } catch (e) { - toast(typeof e == "string" ? e : e.message); - } finally { - // setIsLoading(false); - - dispatch(setAccountIsLoading(false)); - } - }} - > - {user.site.my_user?.local_user_view?.person.actor_id == localPerson.actor_id ? ( - - ) : ( - - )} - {user.site.my_user?.local_user_view?.person.name}@{user.base} - - ); - })} - - )} - {/* - + + } + endDecorator={} sx={{ - color: "text.body", - }} - onClick={() => { - handleClose(); - - queryClient.invalidateQueries({ queryKey: ["lemmyHttp"] }); - dispatch(logoutCurrent()); + mx: 1, + borderRadius: 4, + + // fontSize: "14px", + // overflow: "hidden", + display: "flex", + // flexDirection: "row", + alignItems: "center", + justifyContent: "center", + // gap: 1, }} > - End Session - */} - + + {parsedActor.actorName}@{parsedActor.actorBaseUrl} + {" "} + {userIcon} + + + + {users && users.length > 0 && ( + <> + {users.map((user, index) => { + return ; + })} + + )} + + ); } diff --git a/src/components/Header/ConfigModal.jsx b/src/components/Header/ConfigModal.jsx index 3cd2c75..8a0dae9 100644 --- a/src/components/Header/ConfigModal.jsx +++ b/src/components/Header/ConfigModal.jsx @@ -15,6 +15,7 @@ import ModalClose from "@mui/joy/ModalClose"; import Divider from "@mui/joy/Divider"; import { + selectMandatoryModComment, setConfigItem, setConfigItemJson, selectBlurNsfw, @@ -82,6 +83,7 @@ export default function ConfigModal({ open, onClose }) { const blurNsfw = useSelector(selectBlurNsfw); const showAvatars = useSelector(selectShowAvatars); const nsfwWords = useSelector(selectNsfwWords); + const mandatoryModComment = useSelector(selectMandatoryModComment); const dispatch = useDispatch(); @@ -103,8 +105,8 @@ export default function ConfigModal({ open, onClose }) { })} > - UI Configuration - + Configuration + User Interface )} + Moderation + + dispatch(setConfigItem("mandatoryModComment", e))} + /> + ); diff --git a/src/components/Header/SiteMenu.jsx b/src/components/Header/SiteMenu.jsx index 4bf3d96..b166c2c 100644 --- a/src/components/Header/SiteMenu.jsx +++ b/src/components/Header/SiteMenu.jsx @@ -1,58 +1,21 @@ import React from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useNavigate, useLocation } from "react-router-dom"; -import { useQueryClient } from "@tanstack/react-query"; - -import { Toaster, toast } from "sonner"; - -import { BrowserRouter as Router, useNavigate, useLocation } from "react-router-dom"; - -import Typography from "@mui/joy/Typography"; import Chip from "@mui/joy/Chip"; -import Sheet from "@mui/joy/Sheet"; -import Box from "@mui/joy/Box"; import Button from "@mui/joy/Button"; -import Menu from "@mui/joy/Menu"; -import MenuItem from "@mui/joy/MenuItem"; -import IconButton from "@mui/joy/IconButton"; import CircularProgress from "@mui/joy/CircularProgress"; -import CachedIcon from "@mui/icons-material/Cached"; -import LogoutIcon from "@mui/icons-material/Logout"; -import ArrowDropDown from "@mui/icons-material/ArrowDropDown"; -import OpenInNewIcon from "@mui/icons-material/OpenInNew"; - -// user role icons -import VerifiedUserIcon from "@mui/icons-material/VerifiedUser"; -import SupervisedUserCircleIcon from "@mui/icons-material/SupervisedUserCircle"; -import AccountBoxIcon from "@mui/icons-material/AccountBox"; import DashboardIcon from "@mui/icons-material/Dashboard"; -import SwitchAccountIcon from "@mui/icons-material/SwitchAccount"; - -import GitHubIcon from "@mui/icons-material/GitHub"; - import FlagIcon from "@mui/icons-material/Flag"; import HowToRegIcon from "@mui/icons-material/HowToReg"; -import { logoutCurrent, selectUsers } from "../../reducers/accountReducer"; - -import { LemmyHttp } from "lemmy-js-client"; - -import { useLemmyHttp, refreshAllData } from "../../hooks/useLemmyHttp"; +import { useLemmyHttp } from "../../hooks/useLemmyHttp"; import { getSiteData } from "../../hooks/getSiteData"; -import { HeaderChip } from "../Display.jsx"; import { BasicInfoTooltip } from "../Tooltip.jsx"; -import { parseActorId } from "../../utils.js"; - -import { addUser, setAccountIsLoading, setUsers, setCurrentUser } from "../../reducers/accountReducer"; - export default function SiteMenu() { - // const dispatch = useDispatch(); - // const queryClient = useQueryClient(); - const location = useLocation(); const navigate = useNavigate(); diff --git a/src/components/Header/UserMenu.jsx b/src/components/Header/UserMenu.jsx index 94baa94..fdec2f6 100644 --- a/src/components/Header/UserMenu.jsx +++ b/src/components/Header/UserMenu.jsx @@ -100,27 +100,7 @@ export default function UserMenu() { - - - - - + */} + diff --git a/src/components/Shared/Icons.jsx b/src/components/Shared/Icons.jsx new file mode 100644 index 0000000..fadf6d0 --- /dev/null +++ b/src/components/Shared/Icons.jsx @@ -0,0 +1,26 @@ +import React from "react"; + +// user role icons +import VerifiedUserIcon from "@mui/icons-material/VerifiedUser"; +import SupervisedUserCircleIcon from "@mui/icons-material/SupervisedUserCircle"; +import AccountBoxIcon from "@mui/icons-material/AccountBox"; + +// content icons +import ForumIcon from "@mui/icons-material/Forum"; +import FormatQuoteIcon from "@mui/icons-material/FormatQuote"; + +// approval icons +import HelpIcon from "@mui/icons-material/Help"; + +export const RoleIcons = { + user: (...props) => , + mod: (...props) => , + admin: (...props) => , +}; + +export const ContentIcons = { + post: (...props) => , + comment: (...props) => , + pm: (...props) => , + approval: (...props) => , +}; diff --git a/src/components/Tooltip.jsx b/src/components/Tooltip.jsx index ba0c9cf..0675303 100644 --- a/src/components/Tooltip.jsx +++ b/src/components/Tooltip.jsx @@ -89,7 +89,7 @@ export const UserTooltip = ({ user, ...props }) => { {/* List of actions taken on this user */} - {userModActionsLoading || (userModActionsFetching && Loading...)} + {(userModActionsLoading || userModActionsFetching) && Loading...} {userModActionsError && Error: {userModActionsError.message}} {userModActionsData && ( diff --git a/src/hooks/useLemmyReports.js b/src/hooks/useLemmyReports.js index 1f78642..a17a5c0 100644 --- a/src/hooks/useLemmyReports.js +++ b/src/hooks/useLemmyReports.js @@ -9,7 +9,13 @@ import { useSelector } from "react-redux"; import { LemmyHttp } from "lemmy-js-client"; import useLemmyInfinite from "./useLemmyInfinite"; -import { selectFilterCommunity, selectFilterType, selectOrderBy, selectShowRemoved, selectShowResolved } from "../reducers/configReducer"; +import { + selectFilterCommunity, + selectFilterType, + selectOrderBy, + selectShowRemoved, + selectShowResolved, +} from "../reducers/configReducer"; // gets paginated / infinite list of reports from lemmy export default function useLemmyReports() { @@ -172,10 +178,10 @@ export default function useLemmyReports() { mergedReports.sort((a, b) => { // check for values that are null - if (!a.post_report?.published) return 1; - if (!b.post_report?.published) return -1; + if (!a.time) return 1; + if (!b.time) return -1; - return new Date(b.post_report.published).getTime() - new Date(a.post_report.published).getTime(); + return new Date(b.time).getTime() - new Date(a.time).getTime(); }); console.log("mergedReports", mergedReports); diff --git a/src/reducers/configReducer.js b/src/reducers/configReducer.js index 8356919..afae92d 100644 --- a/src/reducers/configReducer.js +++ b/src/reducers/configReducer.js @@ -92,6 +92,7 @@ export const selectModLogCommunityId = (state) => state.configReducer.modLogComm export const selectOrderBy = (state) => state.configReducer.orderBy; // export const selectPurgeWithoutDelete = (state) => state.configReducer.purgeWithoutDelete; +export const selectMandatoryModComment = (state) => state.configReducer.mandatoryModComment; export const selectBlurNsfw = (state) => state.configReducer.blurNsfw; export const selectShowAvatars = (state) => state.configReducer.showAvatars; export const selectNsfwWords = (state) => state.configReducer.nsfwWords; diff --git a/src/utils.js b/src/utils.js index aa8b1ec..5af7a5a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,3 +1,14 @@ +export function getUserRole(currentUser) { + if (!currentUser) return null; + + // is admin + if (currentUser.site.my_user.local_user_view.person.admin) return "admin"; + + // is mod + if (currentUser.site.my_user.moderates.length > 0) return "mod"; + + return "user"; +} // given a Lemmy actor_id, determine the type, user and base url of the actor // https://lemmy.tgxn.net/u/tgxn // https://lemmy.tgxn.net/c/lemmy