diff --git a/package-lock.json b/package-lock.json index fe4f6c4..bd2923f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lemmy-modder", - "version": "1.3.5", + "version": "1.3.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lemmy-modder", - "version": "1.3.5", + "version": "1.3.7", "license": "MIT", "dependencies": { "@babel/core": "^7.23.2", diff --git a/package.json b/package.json index 0383f59..be861c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-modder", - "version": "1.3.5", + "version": "1.3.7", "description": "Lemmy Moderation App", "author": "tgxn", "license": "MIT", diff --git a/src/components/ApprovalsList.jsx b/src/components/ApprovalsList.jsx index 28327e5..36bb8e4 100644 --- a/src/components/ApprovalsList.jsx +++ b/src/components/ApprovalsList.jsx @@ -16,7 +16,7 @@ import ReportIcon from "@mui/icons-material/Report"; import { MomentAdjustedTimeAgo, SquareChip } from "./Display.jsx"; -import { PersonMetaLine, ReportDetails } from "./ListItem/Common.jsx"; +import { PersonMetaLine } from "./Shared/ActorMeta.jsx"; import { ApproveButton, DenyButton } from "./Actions/RegistrationButtons.jsx"; diff --git a/src/components/Content/Image.jsx b/src/components/Content/Image.jsx index dedbdd1..a9afe9c 100644 --- a/src/components/Content/Image.jsx +++ b/src/components/Content/Image.jsx @@ -1,9 +1,8 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState } from "react"; import { useImage } from "react-image"; import Box from "@mui/joy/Box"; -import Modal from "@mui/joy/Modal"; import CircularProgress from "@mui/joy/CircularProgress"; const ContentSkeleton = React.memo(function ({ radius = "4px" }) { @@ -62,19 +61,12 @@ const ContentError = React.memo(function ({ message = false, bgcolor = "#ff55551 ); }); -export const Image = React.memo(({ imageSrc, nsfw, onClick }) => { +export const Image = React.memo(({ imageSrc, blurPreview, onClick }) => { const { src, isLoading, error } = useImage({ srcList: imageSrc, useSuspense: false, }); - const [open, setOpen] = useState(false); - // const [image, setImage] = useState("false"); - - const handleClose = () => { - setOpen(false); - }; - return ( { // alt={"Banner"} //scaling style={{ - filter: nsfw ? "blur(8px)" : null, // TODO this should use user setting + filter: blurPreview ? "blur(8px)" : null, // TODO this should use user setting // consdytr objectFit: "contain", objectPosition: "center center", @@ -110,7 +102,7 @@ export const Image = React.memo(({ imageSrc, nsfw, onClick }) => { ); }); -export const Video = React.memo(({ imageSrc, nsfw, onClick }) => { +export const Video = React.memo(({ imageSrc, blurPreview, onClick }) => { const [open, setOpen] = useState(false); // const [image, setImage] = useState("false"); @@ -140,7 +132,7 @@ export const Video = React.memo(({ imageSrc, nsfw, onClick }) => { // alt={"Banner"} //scaling style={{ - filter: nsfw ? "blur(8px)" : null, // TODO this should use user setting + filter: blurPreview ? "blur(8px)" : null, // TODO this should use user setting // consdytr objectFit: "contain", objectPosition: "center center", diff --git a/src/components/Content/PostThumb.jsx b/src/components/Content/PostThumb.jsx index 518fd89..6516a02 100644 --- a/src/components/Content/PostThumb.jsx +++ b/src/components/Content/PostThumb.jsx @@ -1,5 +1,7 @@ import React, { useState, useEffect, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; + import Tooltip from "@mui/joy/Tooltip"; import AspectRatio from "@mui/joy/AspectRatio"; import Typography from "@mui/joy/Typography"; @@ -13,6 +15,14 @@ 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"; + function ThumbWrapper({ width = 200, tooltip, modal = null, children }) { return ( <> @@ -30,7 +40,7 @@ function ThumbWrapper({ width = 200, tooltip, modal = null, children }) { ); } -export default function PostThumb({ width = 200, post }) { +export default function PostThumb({ width = 200, post, report }) { /** * look for these in `post` * @@ -41,6 +51,13 @@ export default function PostThumb({ width = 200, post }) { * - embed_description */ + const reportReason = report.reason.toLowerCase() || null; + + console.log("report", reportReason); + + const blurNsfw = useSelector(selectBlurNsfw); + const nsfwWords = useSelector(selectNsfwWords); + const { url, thumbnail_url, nsfw, embed_title, embed_description } = post; const isImage = url.match(/\.(jpeg|jpg|gif|png|webp)$/) != null; @@ -52,6 +69,13 @@ export default function PostThumb({ width = 200, post }) { const handleClose = () => { setOpen(false); }; + + let shouldBlurPreview = false; + if (blurNsfw && nsfw) shouldBlurPreview = true; + if (blurNsfw && nsfwWords.some((word) => reportReason.includes(word))) { + shouldBlurPreview = true; + } + // return image content thumb if (isImage) { return ( @@ -88,7 +112,7 @@ export default function PostThumb({ width = 200, post }) { > { setOpen(true); }} diff --git a/src/components/Display.jsx b/src/components/Display.jsx index e18eae9..474f410 100644 --- a/src/components/Display.jsx +++ b/src/components/Display.jsx @@ -4,8 +4,8 @@ import Moment from "react-moment"; import { sanitizeUrl } from "@braintree/sanitize-url"; +import Avatar from "@mui/joy/Avatar"; import Typography from "@mui/joy/Typography"; -import Tooltip from "@mui/joy/Tooltip"; import Link from "@mui/joy/Link"; import Chip from "@mui/joy/Chip"; @@ -135,3 +135,19 @@ export const FediverseChipLink = ({ href, size = "md", ...props }) => { ); }; + +export function UserAvatar({ source, ...props }) { + return ( + + ); +} diff --git a/src/components/Header/AccountMenu.jsx b/src/components/Header/AccountMenu.jsx new file mode 100644 index 0000000..748d1a6 --- /dev/null +++ b/src/components/Header/AccountMenu.jsx @@ -0,0 +1,160 @@ +import React from "react"; + +import { useDispatch, useSelector } from "react-redux"; + +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 Divider from "@mui/joy/Divider"; +import Switch from "@mui/joy/Switch"; +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 { 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"; + +import ConfigModal from "./ConfigModal.jsx"; + +export default function AccountMenu({ anchorEl, open, onClose }) { + const dispatch = useDispatch(); + + const queryClient = useQueryClient(); + + const users = useSelector(selectUsers); + + const { mutate: refreshMutate } = refreshAllData(); + + const [isLoading, setIsLoading] = React.useState(false); + + const { baseUrl, siteData, localPerson, userRole } = getSiteData(); + + let userTooltip = "user"; + let userIcon = ; + if (userRole == "admin") { + userTooltip = "admin"; + userIcon = ; + } + if (userRole == "mod") { + userTooltip = "mod"; + userIcon = ; + } + + 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} + + ); + })} + + )} + {/* + { + handleClose(); + + queryClient.invalidateQueries({ queryKey: ["lemmyHttp"] }); + dispatch(logoutCurrent()); + }} + > + End Session + */} + + ); +} diff --git a/src/components/Header/CenterMenu.jsx b/src/components/Header/CenterMenu.jsx new file mode 100644 index 0000000..8e4b22c --- /dev/null +++ b/src/components/Header/CenterMenu.jsx @@ -0,0 +1,90 @@ +import React from "react"; + +import Typography from "@mui/joy/Typography"; + +import Box from "@mui/joy/Box"; +import Button from "@mui/joy/Button"; +import IconButton from "@mui/joy/IconButton"; + +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import GitHubIcon from "@mui/icons-material/GitHub"; +import SettingsIcon from "@mui/icons-material/Settings"; +import { getSiteData } from "../../hooks/getSiteData"; + +import { BasicInfoTooltip } from "../Tooltip.jsx"; + +import ConfigModal from "./ConfigModal.jsx"; + +export default function CenterMenu() { + const { baseUrl, siteData, localPerson, userRole } = getSiteData(); + + const [open, setOpen] = React.useState(false); + + return ( + + {siteData && ( + + + + )} + + Code & Issues on GitHub
+ Version: {process.env.PACKAGE_VERSION} + + } + variant="outlined" + > + + + +
+ + setOpen(true)} + > + + + + setOpen(false)} /> +
+ ); +} diff --git a/src/components/Header/ConfigModal.jsx b/src/components/Header/ConfigModal.jsx new file mode 100644 index 0000000..3cd2c75 --- /dev/null +++ b/src/components/Header/ConfigModal.jsx @@ -0,0 +1,141 @@ +import * as React from "react"; + +import { useDispatch, useSelector } from "react-redux"; + +import FormControl from "@mui/joy/FormControl"; +import FormLabel from "@mui/joy/FormLabel"; +import FormHelperText from "@mui/joy/FormHelperText"; +import Autocomplete from "@mui/joy/Autocomplete"; +import Box from "@mui/joy/Box"; +import Modal from "@mui/joy/Modal"; +import ModalDialog from "@mui/joy/ModalDialog"; +import Switch from "@mui/joy/Switch"; +import DialogTitle from "@mui/joy/DialogTitle"; +import ModalClose from "@mui/joy/ModalClose"; +import Divider from "@mui/joy/Divider"; + +import { + setConfigItem, + setConfigItemJson, + selectBlurNsfw, + selectShowAvatars, + selectNsfwWords, +} from "../../reducers/configReducer"; + +function BooleanSetting({ label, subtext, value, onChange }) { + return ( + +
+ {label} + {subtext} +
+ onChange(event.target.checked)} + color={value ? "success" : "neutral"} + variant={value ? "solid" : "outlined"} + endDecorator={value ? "On" : "Off"} + slotProps={{ + endDecorator: { + sx: { + minWidth: 24, + }, + }, + }} + /> +
+ ); +} + +function ArraySetting({ label, subtext, value, onChange }) { + return ( + +
+ {label} + {/* {subtext} */} +
+ option} + defaultValue={[...value]} + onChange={(e, newval, reason) => { + console.log("onChange", newval, reason); + onChange(newval); + }} + /> +
+ ); +} + +export default function ConfigModal({ open, onClose }) { + const blurNsfw = useSelector(selectBlurNsfw); + const showAvatars = useSelector(selectShowAvatars); + const nsfwWords = useSelector(selectNsfwWords); + + const dispatch = useDispatch(); + + return ( + + ({ + [theme.breakpoints.only("xs")]: { + top: "unset", + bottom: 0, + left: 0, + right: 0, + borderRadius: 0, + transform: "none", + maxWidth: "unset", + }, + })} + > + + UI Configuration + + + dispatch(setConfigItem("showAvatars", e))} + /> + dispatch(setConfigItem("blurNsfw", e))} + /> + {blurNsfw && ( + dispatch(setConfigItemJson("nsfwWords", e))} + /> + )} + + + + ); +} diff --git a/src/components/Header/SiteMenu.jsx b/src/components/Header/SiteMenu.jsx new file mode 100644 index 0000000..4bf3d96 --- /dev/null +++ b/src/components/Header/SiteMenu.jsx @@ -0,0 +1,247 @@ +import React from "react"; + +import { useDispatch, useSelector } from "react-redux"; + +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 { 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(); + + const { baseUrl, siteData, localPerson, userRole } = getSiteData(); + + const { + isLoading: reportCountsLoading, + isFetching: reportCountsFetching, + error: reportCountsError, + data: reportCountsData, + } = useLemmyHttp("getReportCount"); + + const { + isLoading: regAppCountIsLoading, + isFetching: regAppCountIsFetching, + error: regCountAppError, + data: regCountAppData, + } = useLemmyHttp("getUnreadRegistrationApplicationCount"); + + // let userTooltip = "You are a regular user"; + // if (userRole == "admin") userTooltip = "You are a site admin"; + // if (userRole == "mod") userTooltip = "You are a community moderator"; + + let totalReports = reportCountsData?.post_reports + reportCountsData?.comment_reports; + if (userRole == "admin") totalReports += reportCountsData?.private_message_reports; + + return ( + <> + + + + + {userRole != "user" && ( + + + + )} + + {userRole == "admin" && ( + + + + )} + + + + + + ); +} diff --git a/src/components/Header/UserMenu.jsx b/src/components/Header/UserMenu.jsx new file mode 100644 index 0000000..94baa94 --- /dev/null +++ b/src/components/Header/UserMenu.jsx @@ -0,0 +1,145 @@ +import React from "react"; + +import { useDispatch, useSelector } from "react-redux"; + +import { useQueryClient, useIsFetching } from "@tanstack/react-query"; + +import Button from "@mui/joy/Button"; +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"; + +// 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 { logoutCurrent, selectUsers } from "../../reducers/accountReducer"; + +import { useLemmyHttp, refreshAllData } from "../../hooks/useLemmyHttp"; +import { getSiteData } from "../../hooks/getSiteData"; + +import { BasicInfoTooltip } from "../Tooltip.jsx"; + +import { parseActorId } from "../../utils.js"; +import AccountMenu from "./AccountMenu.jsx"; + +import { selectAccountIsLoading } from "../../reducers/accountReducer"; + +export default function UserMenu() { + const dispatch = useDispatch(); + + const queryClient = useQueryClient(); + + // const users = useSelector(selectUsers); + const accountIsLoading = useSelector(selectAccountIsLoading); + + const { mutate: refreshMutate } = refreshAllData(); + const isFetching = useIsFetching(); + + const { baseUrl, siteData, localPerson, userRole } = getSiteData(); + + const [anchorEl, setAnchorEl] = React.useState(null); + const [menuOpen, setMenuOpen] = React.useState(false); + + const handleClick = (event) => { + if (menuOpen) return handleClose(); + + setAnchorEl(event.currentTarget); + setMenuOpen(true); + }; + + const handleClose = () => { + setMenuOpen(false); + setAnchorEl(null); + }; + + let userTooltip = "user"; + let userIcon = ; + if (userRole == "admin") { + userTooltip = "admin"; + userIcon = ; + } + if (userRole == "mod") { + userTooltip = "mod"; + userIcon = ; + } + + const anythingLoading = accountIsLoading == true || isFetching == true; + + const parsedActor = parseActorId(localPerson.actor_id); + + return ( + <> + + { + refreshMutate(); + }} + > + {!anythingLoading && } + {anythingLoading && ( + + )} + + + + + + + + + + + { + handleClose(); + + queryClient.invalidateQueries({ queryKey: ["lemmyHttp"] }); + dispatch(logoutCurrent()); + }} + sx={{ + borderRadius: 4, + }} + > + + + + + ); +} diff --git a/src/components/ListItem/Comment.jsx b/src/components/ListItem/Comment.jsx index 7715fb1..cf4841f 100644 --- a/src/components/ListItem/Comment.jsx +++ b/src/components/ListItem/Comment.jsx @@ -15,7 +15,8 @@ import { SquareChip } from "../Display.jsx"; import { ResolveCommentReportButton, RemoveCommentButton } from "../Actions/CommentButtons.jsx"; import { BanUserCommunityButton, BanUserSiteButton, PurgeUserSiteButton } from "../Actions/GenButtons.jsx"; -import { PersonMetaLine, CommunityMetaLine, ReportDetails } from "./Common.jsx"; +import { PersonMetaLine, CommunityMetaLine } from "../Shared/ActorMeta.jsx"; +import { ReportDetails } from "../Shared/ReportDetails.jsx"; import { MomentAdjustedTimeAgo, SanitizedLink, FediverseChipLink, UpvoteDownvoteChip } from "../Display.jsx"; @@ -157,7 +158,7 @@ export default function CommentListItem({ report }) { {/* */} - + diff --git a/src/components/ListItem/PM.jsx b/src/components/ListItem/PM.jsx index b8be4c4..564d989 100644 --- a/src/components/ListItem/PM.jsx +++ b/src/components/ListItem/PM.jsx @@ -14,7 +14,8 @@ import { MomentAdjustedTimeAgo, SquareChip } from "../Display.jsx"; import { ResolvePMReportButton } from "../Actions/PMButtons.jsx"; import { BanUserSiteButton, PurgeUserSiteButton } from "../Actions/GenButtons.jsx"; -import { PersonMetaLine, ReportDetails } from "./Common.jsx"; +import { PersonMetaLine, CommunityMetaLine } from "../Shared/ActorMeta.jsx"; +import { ReportDetails } from "../Shared/ReportDetails.jsx"; const PMContentDetail = ({ report }) => { return ( diff --git a/src/components/ListItem/Post.jsx b/src/components/ListItem/Post.jsx index 83acd6a..c6b202d 100644 --- a/src/components/ListItem/Post.jsx +++ b/src/components/ListItem/Post.jsx @@ -26,7 +26,8 @@ import { ResolvePostReportButton, RemovePostButton, PurgePostButton } from "../A import { BanUserCommunityButton, BanUserSiteButton, PurgeUserSiteButton } from "../Actions/GenButtons.jsx"; -import { PersonMetaLine, CommunityMetaLine, ReportDetails } from "./Common.jsx"; +import { PersonMetaLine, CommunityMetaLine } from "../Shared/ActorMeta.jsx"; +import { ReportDetails } from "../Shared/ReportDetails.jsx"; import { getSiteData } from "../../hooks/getSiteData"; @@ -186,7 +187,7 @@ export default function PostListItem({ report }) { flexShrink: 0, }} > - +
)} @@ -218,7 +219,7 @@ export default function PostListItem({ report }) { {/* @TODO Maybe only show purge is post is deleted?? */} - + diff --git a/src/components/ReportsList.jsx b/src/components/ReportsList.jsx index 7d58b82..8e64022 100644 --- a/src/components/ReportsList.jsx +++ b/src/components/ReportsList.jsx @@ -1,6 +1,14 @@ import React from "react"; +import Card from "@mui/joy/Card"; + import Box from "@mui/joy/Box"; +import Badge from "@mui/joy/Badge"; +import Tooltip from "@mui/joy/Tooltip"; + +import StickyNote2Icon from "@mui/icons-material/StickyNote2"; +import ForumIcon from "@mui/icons-material/Forum"; +import DraftsIcon from "@mui/icons-material/Drafts"; import SoapIcon from "@mui/icons-material/Soap"; @@ -8,7 +16,104 @@ import PostReportItem from "./ListItem/Post.jsx"; import CommentReportItem from "./ListItem/Comment.jsx"; import PMReportItem from "./ListItem/PM.jsx"; -import { ReportListItem } from "./ListItem/Common.jsx"; +function ReportListItem({ itemType, report, children }) { + let itemColor; + let itemIcon; + let resolved = true; + + // const parsedActor = parseActorId(report.actor_id); + + if (itemType == "post") { + resolved = report.post_report.resolved; + itemColor = "primary"; + itemIcon = ( + + + + ); + } else if (itemType == "comment") { + resolved = report.comment_report.resolved; + itemColor = "success"; + itemIcon = ( + + + + ); + } else if (itemType == "pm") { + resolved = report.private_message_report.resolved; + itemColor = "warning"; + itemIcon = ( + + + + ); + } + + return ( + + + {/* {isFetching && ( + + Loading... + + )} */} + {children} + + + ); +} export default function ReportsList({ reportsList }) { if (!reportsList || reportsList.length == 0) { diff --git a/src/components/ListItem/Common.jsx b/src/components/Shared/ActorMeta.jsx similarity index 56% rename from src/components/ListItem/Common.jsx rename to src/components/Shared/ActorMeta.jsx index de70dde..a08fec5 100644 --- a/src/components/ListItem/Common.jsx +++ b/src/components/Shared/ActorMeta.jsx @@ -1,150 +1,39 @@ import React from "react"; -import Alert from "@mui/joy/Alert"; -import Card from "@mui/joy/Card"; +import { useDispatch, useSelector } from "react-redux"; -import Avatar from "@mui/joy/Avatar"; import Box from "@mui/joy/Box"; import Typography from "@mui/joy/Typography"; -import Badge from "@mui/joy/Badge"; import Tooltip from "@mui/joy/Tooltip"; - import Link from "@mui/joy/Link"; -import StickyNote2Icon from "@mui/icons-material/StickyNote2"; -import ForumIcon from "@mui/icons-material/Forum"; -import DraftsIcon from "@mui/icons-material/Drafts"; - import SecurityIcon from "@mui/icons-material/Security"; import BlockIcon from "@mui/icons-material/Block"; import SmartToyIcon from "@mui/icons-material/SmartToy"; import DeleteIcon from "@mui/icons-material/Delete"; -import { SquareChip, MomentAdjustedTimeAgo, SanitizedLink, FediverseChipLink } from "../Display.jsx"; +import { SquareChip, UserAvatar, FediverseChipLink } from "../Display.jsx"; import { UserTooltip } from "../Tooltip.jsx"; import { parseActorId } from "../../utils.js"; import { getSiteData } from "../../hooks/getSiteData"; -export function UserAvatar({ source, ...props }) { - return ( - - ); -} - -export function ReportListItem({ itemType, report, children }) { - let itemColor; - let itemIcon; - let resolved = true; +import { PersonMetaChips } from "./UserChips.jsx"; - // const parsedActor = parseActorId(report.actor_id); - - if (itemType == "post") { - resolved = report.post_report.resolved; - itemColor = "primary"; - itemIcon = ( - - - - ); - } else if (itemType == "comment") { - resolved = report.comment_report.resolved; - itemColor = "success"; - itemIcon = ( - - - - ); - } else if (itemType == "pm") { - resolved = report.private_message_report.resolved; - itemColor = "warning"; - itemIcon = ( - - - - ); - } - - return ( - - - {/* {isFetching && ( - - Loading... - - )} */} - {children} - - - ); -} +import { + setConfigItem, + setConfigItemJson, + selectBlurNsfw, + selectShowAvatars, + selectNsfwWords, +} from "../../reducers/configReducer"; export function PersonMetaLine({ creator, by = false, sx }) { const { baseUrl, siteData, localPerson, userRole } = getSiteData(); + const showAvatars = useSelector(selectShowAvatars); + const actorInstanceBaseUrl = creator.actor_id.split("/")[2]; const fediverseUserLink = creator.actor_id; @@ -173,7 +62,7 @@ export function PersonMetaLine({ creator, by = false, sx }) { }} > {by && by} - + {showAvatars && } {creator.display_name && ( {creator.display_name} )} @@ -182,7 +71,7 @@ export function PersonMetaLine({ creator, by = false, sx }) { variant="outlined" title={} arrow - disableInteractive + // disableInteractive > @@ -196,7 +85,8 @@ export function PersonMetaLine({ creator, by = false, sx }) { {/* Post Author Meta */} - + + {/* {baseUrl != actorInstanceBaseUrl && } {creator.admin && ( @@ -222,7 +112,7 @@ export function PersonMetaLine({ creator, by = false, sx }) { {creator.deleted && ( } /> )} - + */} ); } @@ -295,37 +185,3 @@ export function CommunityMetaLine({ community, showIn = false, sx }) { ); } - -export function ReportDetails({ report, creator }) { - return ( - -
- - {report.published && ( - - Reported {report.published} - - )} - - - - - {report.reason} - -
-
- ); -} diff --git a/src/components/Shared/ReportDetails.jsx b/src/components/Shared/ReportDetails.jsx new file mode 100644 index 0000000..a7117ad --- /dev/null +++ b/src/components/Shared/ReportDetails.jsx @@ -0,0 +1,42 @@ +import React from "react"; + +import Alert from "@mui/joy/Alert"; +import Typography from "@mui/joy/Typography"; + +import { SquareChip, MomentAdjustedTimeAgo } from "../Display.jsx"; + +import { PersonMetaLine } from "../Shared/ActorMeta.jsx"; + +export function ReportDetails({ report, creator }) { + return ( + +
+ + {report.published && ( + + Reported {report.published} + + )} + + + + + {report.reason} + +
+
+ ); +} diff --git a/src/components/Shared/UserChips.jsx b/src/components/Shared/UserChips.jsx new file mode 100644 index 0000000..7da6adc --- /dev/null +++ b/src/components/Shared/UserChips.jsx @@ -0,0 +1,79 @@ +import React from "react"; + +import Box from "@mui/joy/Box"; +import Typography from "@mui/joy/Typography"; +import Tooltip from "@mui/joy/Tooltip"; +import Link from "@mui/joy/Link"; + +import SecurityIcon from "@mui/icons-material/Security"; +import BlockIcon from "@mui/icons-material/Block"; +import SmartToyIcon from "@mui/icons-material/SmartToy"; +import DeleteIcon from "@mui/icons-material/Delete"; + +import { SquareChip, UserAvatar, FediverseChipLink } from "../Display.jsx"; + +import { UserTooltip } from "../Tooltip.jsx"; + +import { parseActorId } from "../../utils.js"; + +import { getSiteData } from "../../hooks/getSiteData"; + +export function PersonMetaChips({ person }) { + const { baseUrl } = getSiteData(); + + const actorInstanceBaseUrl = person.actor_id.split("/")[2]; + const fediverseUserLink = person.actor_id; + + let localUserLink = `https://${baseUrl}/u/${person.name}`; + if (baseUrl != actorInstanceBaseUrl) localUserLink = `${localUserLink}@${actorInstanceBaseUrl}`; + + return ( + + {baseUrl != actorInstanceBaseUrl && } + + + + + + + + + + ); +} + +export function SiteAdminChip({ person }) { + if (!person.admin) return null; + + return ( + } + /> + ); +} + +export function BannedUserChip({ person }) { + if (!person.banned) return null; + + return } />; +} + +export function BotAccountChip({ person }) { + if (!person.bot_account) return null; + + return ( + } + /> + ); +} +export function DeletedUserChip({ person }) { + if (!person.deleted) return null; + + return } />; +} diff --git a/src/components/SiteHeader.jsx b/src/components/SiteHeader.jsx index b3dd1a1..c5dace0 100644 --- a/src/components/SiteHeader.jsx +++ b/src/components/SiteHeader.jsx @@ -1,442 +1,13 @@ import React from "react"; -import { useDispatch, useSelector } from "react-redux"; - -import { useQueryClient, useIsFetching } 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 { LemmyHttp } from "lemmy-js-client"; - -import { useLemmyHttp, refreshAllData } from "../hooks/useLemmyHttp"; import { getSiteData } from "../hooks/getSiteData"; -import { HeaderChip } from "./Display.jsx"; -import { BasicInfoTooltip } from "./Tooltip.jsx"; - -import { parseActorId } from "../utils.js"; - -import { - setAccountIsLoading, - setCurrentUser, - logoutCurrent, - selectUsers, - selectAccountIsLoading, -} from "../reducers/accountReducer"; - -function SiteMenu() { - // const dispatch = useDispatch(); - // const queryClient = useQueryClient(); - - const location = useLocation(); - const navigate = useNavigate(); - - const { baseUrl, siteData, localPerson, userRole } = getSiteData(); - - const { - isLoading: reportCountsLoading, - isFetching: reportCountsFetching, - error: reportCountsError, - data: reportCountsData, - } = useLemmyHttp("getReportCount"); - - const { - isLoading: regAppCountIsLoading, - isFetching: regAppCountIsFetching, - error: regCountAppError, - data: regCountAppData, - } = useLemmyHttp("getUnreadRegistrationApplicationCount"); - - // let userTooltip = "You are a regular user"; - // if (userRole == "admin") userTooltip = "You are a site admin"; - // if (userRole == "mod") userTooltip = "You are a community moderator"; - - let totalReports = reportCountsData?.post_reports + reportCountsData?.comment_reports; - if (userRole == "admin") totalReports += reportCountsData?.private_message_reports; - - return ( - <> - - - - - {userRole != "user" && ( - - - - )} - - {userRole == "admin" && ( - - - - )} - - - - - - ); -} - -function UserMenu() { - const dispatch = useDispatch(); - - const queryClient = useQueryClient(); - - const users = useSelector(selectUsers); - const accountIsLoading = useSelector(selectAccountIsLoading); - - const { mutate: refreshMutate } = refreshAllData(); - const isFetching = useIsFetching(); - - const { baseUrl, siteData, localPerson, userRole } = getSiteData(); - - const [anchorEl, setAnchorEl] = React.useState(null); - const [menuOpen, setMenuOpen] = React.useState(false); - - const handleClick = (event) => { - if (menuOpen) return handleClose(); - - setAnchorEl(event.currentTarget); - setMenuOpen(true); - }; - - const handleClose = () => { - setMenuOpen(false); - setAnchorEl(null); - }; - - let userTooltip = "user"; - let userIcon = ; - if (userRole == "admin") { - userTooltip = "admin"; - userIcon = ; - } - if (userRole == "mod") { - userTooltip = "mod"; - userIcon = ; - } - - const anythingLoading = accountIsLoading == true || isFetching == true; - - const parsedActor = parseActorId(localPerson.actor_id); - - return ( - <> - - { - refreshMutate(); - }} - > - {!anythingLoading && } - {anythingLoading && ( - - )} - - - - - - - - {users && users.length > 0 && ( - <> - {users.map((user, index) => { - return ( - { - handleClose(); - - 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, - }); - - 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} - - ); - })} - - )} - {/* - { - handleClose(); - - queryClient.invalidateQueries({ queryKey: ["lemmyHttp"] }); - dispatch(logoutCurrent()); - }} - > - End Session - */} - - - - { - handleClose(); - - queryClient.invalidateQueries({ queryKey: ["lemmyHttp"] }); - dispatch(logoutCurrent()); - }} - sx={{ - borderRadius: 4, - }} - > - - - - - ); -} +import SiteMenu from "./Header/SiteMenu.jsx"; +import UserMenu from "./Header/UserMenu.jsx"; +import CenterMenu from "./Header/CenterMenu.jsx"; export default function SiteHeader({ height }) { const { baseUrl, siteData, localPerson, userRole } = getSiteData(); @@ -473,48 +44,7 @@ export default function SiteHeader({ height }) { alignItems: "center", }} > - {siteData && ( - - - - )} - - Code & Issues on GitHub
- Version: {process.env.PACKAGE_VERSION} -
- } - variant="outlined" - > - - - - + {siteData && ( diff --git a/src/components/Tooltip.jsx b/src/components/Tooltip.jsx index e76cf9e..ba0c9cf 100644 --- a/src/components/Tooltip.jsx +++ b/src/components/Tooltip.jsx @@ -1,73 +1,121 @@ import React from "react"; +import { useDispatch, useSelector } from "react-redux"; + import Alert from "@mui/joy/Alert"; -import Card from "@mui/joy/Card"; import Box from "@mui/joy/Box"; import Typography from "@mui/joy/Typography"; -import Badge from "@mui/joy/Badge"; import Tooltip from "@mui/joy/Tooltip"; - -import Link from "@mui/joy/Link"; +import List from "@mui/joy/List"; +import ListItem from "@mui/joy/ListItem"; +import Divider from "@mui/joy/Divider"; +import ListItemDecorator from "@mui/joy/ListItemDecorator"; import Chip from "@mui/joy/Chip"; -import AdjustIcon from "@mui/icons-material/Adjust"; import SecurityIcon from "@mui/icons-material/Security"; import BlockIcon from "@mui/icons-material/Block"; import SmartToyIcon from "@mui/icons-material/SmartToy"; import DeleteIcon from "@mui/icons-material/Delete"; -import { MomentAdjustedTimeAgo } from "./Display.jsx"; +import { useLemmyHttp, refreshAllData } from "../hooks/useLemmyHttp"; +import { UserAvatar, MomentAdjustedTimeAgo } from "./Display.jsx"; + +import { PersonMetaChips } from "./Shared/UserChips.jsx"; + +import { + setConfigItem, + setConfigItemJson, + selectBlurNsfw, + selectShowAvatars, + selectNsfwWords, +} from "../reducers/configReducer"; export const UserTooltip = ({ user, ...props }) => { + console.log("user", user); + const showAvatars = useSelector(selectShowAvatars); + + // get user modlog entries + const { + isLoading: userModActionsLoading, + isFetching: userModActionsFetching, + error: userModActionsError, + data: userModActionsData, + } = useLemmyHttp("getModlog", { + other_person_id: user.id, + }); + + const fullUserString = `${user.name}@${user.actor_id.split("/")[2]}`; + return ( - - - {user.name}@{user.actor_id.split("/")[2]} - {" "} - {user.display_name && ` ${user.display_name}`} - + + {showAvatars && } + + {user.display_name && user.display_name} + {!user.display_name && fullUserString} + + + {user.display_name && {fullUserString}} {user.published && ( - + registered {user.published} )} - {user.admin && ( - }> - Admin Account - - )} - - {user.banned && ( - }> - Banned Account - - )} - - {user.bot_account && ( - }> - Bot Account - - )} - - {user.deleted && ( - }> - Deleted Account - - )} + User Mod Activity + + {/* List of actions taken on this user */} + + {userModActionsLoading || (userModActionsFetching && Loading...)} + {userModActionsError && Error: {userModActionsError.message}} + {userModActionsData && ( + + {Object.keys(userModActionsData).map( + (action) => + userModActionsData[action].length > 0 && ( + {userModActionsData[action].length}} + > + {/* + {userModActionsData[action].length} + */} + {action} + + ), + )} + + )} + + + User Info + + + + ); }; diff --git a/src/hooks/useLemmyHttp.js b/src/hooks/useLemmyHttp.js index a7a32d8..7ea96f6 100644 --- a/src/hooks/useLemmyHttp.js +++ b/src/hooks/useLemmyHttp.js @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -9,13 +9,23 @@ import { LemmyHttp } from "lemmy-js-client"; import { selectCurrentUser, updateCurrentUserData, setAccountIsLoading } from "../reducers/accountReducer"; -export function useLemmyHttp(callLemmyMethod, formData) { +export function useLemmyHttp(callLemmyMethod, formData = {}) { const currentUser = useSelector(selectCurrentUser); + const formDataArray = useMemo(() => { + const formDataArray = []; + for (const [key, value] of Object.entries(formData)) { + formDataArray.push(key); + formDataArray.push(value); + } + return formDataArray; + }, [formData]); + + const { baseUrl, siteData, localPerson, userRole } = getSiteData(); const { isSuccess, isLoading, isError, error, data, isFetching, refetch } = useQuery({ - queryKey: ["lemmyHttp", localPerson.id, callLemmyMethod], + queryKey: ["lemmyHttp", localPerson.id, callLemmyMethod, formDataArray], queryFn: async () => { const lemmyClient = new LemmyHttp(`https://${currentUser.base}`); diff --git a/src/reducers/configReducer.js b/src/reducers/configReducer.js index 6ffc475..8356919 100644 --- a/src/reducers/configReducer.js +++ b/src/reducers/configReducer.js @@ -5,34 +5,55 @@ export function setConfigItem(configKey, configValue) { payload: { configKey, configValue }, }; } +export function setConfigItemJson(configKey, configValue) { + console.log("setConfigItemJson", configKey, configValue); + return { + type: "setConfigItemJson", + payload: { configKey, configValue }, + }; +} + +function loadWithDefault(key, defaultValue = null, parseJson = true) { + const storedValue = localStorage.getItem(key); + + if (storedValue) { + try { + return JSON.parse(storedValue); + } catch (e) { + console.error("Error parsiong json config", e); + return storedValue; + } + } else { + return defaultValue; + } +} const initialState = { isInElectron: window.modder ? true : false, - orderBy: localStorage.getItem("config.orderBy") || "hot", + orderBy: loadWithDefault("config.orderBy", "hot"), + + modLogType: loadWithDefault("config.modLogType", "all"), + modLogLocal: loadWithDefault("config.modLogLocal", true), - modLogType: localStorage.getItem("config.modLogType") || "all", - modLogLocal: localStorage.getItem("config.modLogLocal") || true, + filterType: loadWithDefault("config.filterType", "all"), - filterType: localStorage.getItem("config.filterType") || "all", - filterCommunity: localStorage.getItem("config.filterCommunity") || "all", - showResolved: localStorage.getItem("config.showResolved") - ? JSON.parse(localStorage.getItem("config.showResolved")) - : false, - showRemoved: localStorage.getItem("config.showRemoved") - ? JSON.parse(localStorage.getItem("config.showRemoved")) - : true, - hideReadApprovals: localStorage.getItem("config.hideReadApprovals") - ? JSON.parse(localStorage.getItem("config.hideReadApprovals")) - : true, + filterCommunity: loadWithDefault("config.filterCommunity", "all"), + + showResolved: loadWithDefault("config.showResolved", false), + showRemoved: loadWithDefault("config.showRemoved", true), + + hideReadApprovals: loadWithDefault("config.hideReadApprovals", true), // are comments required on mod actions? - mandatoryModComment: localStorage.getItem("config.mandatoryModComment") - ? JSON.parse(localStorage.getItem("config.mandatoryModComment")) - : false, + mandatoryModComment: loadWithDefault("config.mandatoryModComment", false), // can you purge contenbt wihtout removing it first - purgeWithoutDelete: localStorage.getItem("config.purgeWithoutDelete"), + // purgeWithoutDelete: loadWithDefault("config.purgeWithoutDelete", false), + + blurNsfw: loadWithDefault("config.blurNsfw", true), + showAvatars: loadWithDefault("config.showAvatars", true), + nsfwWords: loadWithDefault("config.nsfwWords", []), }; const configReducer = (state = initialState, action = {}) => { @@ -45,6 +66,14 @@ const configReducer = (state = initialState, action = {}) => { localStorage.setItem(`config.${action.payload.configKey}`, action.payload.configValue); return newConfig; + case "setConfigItemJson": + const newConfigJson = { + ...state, + [action.payload.configKey]: action.payload.configValue, + }; + localStorage.setItem(`config.${action.payload.configKey}`, JSON.stringify(action.payload.configValue)); + return newConfigJson; + default: return state; } @@ -52,7 +81,6 @@ const configReducer = (state = initialState, action = {}) => { export default configReducer; - export const selectIsInElectron = (state) => state.configReducer.isInElectron; export const selectFilterCommunity = (state) => state.configReducer.filterCommunity; export const selectFilterType = (state) => state.configReducer.filterType; @@ -61,4 +89,9 @@ export const selectShowResolved = (state) => state.configReducer.showResolved; export const selectHideReadApprovals = (state) => state.configReducer.hideReadApprovals; export const selectShowRemoved = (state) => state.configReducer.showRemoved; export const selectModLogCommunityId = (state) => state.configReducer.modLogCommunityId; -export const selectOrderBy = (state) => state.configReducer.orderBy; \ No newline at end of file +export const selectOrderBy = (state) => state.configReducer.orderBy; + +// export const selectPurgeWithoutDelete = (state) => state.configReducer.purgeWithoutDelete; +export const selectBlurNsfw = (state) => state.configReducer.blurNsfw; +export const selectShowAvatars = (state) => state.configReducer.showAvatars; +export const selectNsfwWords = (state) => state.configReducer.nsfwWords;