diff --git a/package-lock.json b/package-lock.json index 3f52dee..f664850 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lemmy-modder", - "version": "1.3.8", + "version": "1.3.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lemmy-modder", - "version": "1.3.8", + "version": "1.3.9", "license": "MIT", "dependencies": { "@babel/core": "^7.23.2", @@ -40,6 +40,8 @@ "react-redux": "^8.1.1", "react-router-dom": "^6.18.0", "redux": "^4.2.1", + "redux-persist": "^6.0.0", + "remove-markdown": "^0.5.0", "sass": "^1.69.5", "sass-loader": "^13.3.2", "sonner": "^1.1.0", @@ -7157,6 +7159,14 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "peerDependencies": { + "redux": ">4.0.0" + } + }, "node_modules/redux-thunk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", @@ -7238,6 +7248,11 @@ "node": ">= 0.10" } }, + "node_modules/remove-markdown": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.5.0.tgz", + "integrity": "sha512-x917M80K97K5IN1L8lUvFehsfhR8cYjGQ/yAMRI9E7JIKivtl5Emo5iD13DhMr+VojzMCiYk8V2byNPwT/oapg==" + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", diff --git a/package.json b/package.json index ab02794..39ade2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-modder", - "version": "1.3.8", + "version": "1.3.9", "description": "Lemmy Moderation App", "author": "tgxn", "license": "MIT", @@ -45,6 +45,8 @@ "react-redux": "^8.1.1", "react-router-dom": "^6.18.0", "redux": "^4.2.1", + "remove-markdown": "^0.5.0", + "redux-persist": "^6.0.0", "sass": "^1.69.5", "sass-loader": "^13.3.2", "sonner": "^1.1.0", diff --git a/src/App.jsx b/src/App.jsx index 07efe35..6cbed6a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -21,11 +21,12 @@ import Dashboard from "./pages/Dashboard"; import Actions from "./pages/Actions"; import Approvals from "./pages/Approvals"; import Reports from "./pages/Reports"; +import Messages from "./pages/Messages"; import Login from "./pages/Login"; -import AppStore from "./store"; -import { selectIsInElectron } from "./reducers/configReducer"; -import { selectCurrentUser } from "./reducers/accountReducer"; +import { store } from "./redux/store"; +import { selectIsInElectron } from "./redux/reducer/configReducer"; +import { selectCurrentUser } from "./redux/reducer/accountReducer"; function PageRouter() { const theme = useTheme(); @@ -182,6 +183,48 @@ function PageRouter() { } /> + + + + + + } + /> + + + + + + } + /> @@ -195,7 +238,7 @@ export default function App() { - + diff --git a/src/components/Actions/CommentButtons.jsx b/src/components/Actions/CommentButtons.jsx index 46f2dce..0735c54 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, selectMandatoryModComment } from "../../reducers/configReducer.js"; +import { selectShowResolved, selectMandatoryModComment } from "../../redux/reducer/configReducer.js"; export const ResolveCommentReportButton = ({ report, ...props }) => { const queryClient = useQueryClient(); diff --git a/src/components/Actions/GenButtons.jsx b/src/components/Actions/GenButtons.jsx index b0ea57f..9cc5b90 100644 --- a/src/components/Actions/GenButtons.jsx +++ b/src/components/Actions/GenButtons.jsx @@ -16,7 +16,7 @@ import { ConfirmDialog, } from "./BaseElements.jsx"; -import { selectMandatoryModComment } from "../../reducers/configReducer"; +import { selectMandatoryModComment } from "../../redux/reducer/configReducer"; // banFromCommunity export const BanUserCommunityButton = ({ person, community, isBanned, ...props }) => { diff --git a/src/components/Actions/PMButtons.jsx b/src/components/Actions/PMButtons.jsx index 1f17645..fef7586 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, selectMandatoryModComment } from "../../reducers/configReducer.js"; +import { selectShowResolved, selectMandatoryModComment } from "../../redux/reducer/configReducer.js"; // allow resolving / unresolving a post report // resolvePrivateMessageReport diff --git a/src/components/Actions/PostButtons.jsx b/src/components/Actions/PostButtons.jsx index 8fc40af..b22e19e 100644 --- a/src/components/Actions/PostButtons.jsx +++ b/src/components/Actions/PostButtons.jsx @@ -11,9 +11,9 @@ 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 } from "../../redux/reducer/configReducer.js"; -import { selectMandatoryModComment } from "../../reducers/configReducer"; +import { selectMandatoryModComment } from "../../redux/reducer/configReducer"; // allow resolving / unresolving a post report export const ResolvePostReportButton = ({ report, ...props }) => { diff --git a/src/components/Actions/RegistrationButtons.jsx b/src/components/Actions/RegistrationButtons.jsx index d789eff..eeb596e 100644 --- a/src/components/Actions/RegistrationButtons.jsx +++ b/src/components/Actions/RegistrationButtons.jsx @@ -13,7 +13,7 @@ import { useLemmyHttpAction } from "../../hooks/useLemmyHttp.js"; import { getSiteData } from "../../hooks/getSiteData"; import { BaseActionButton, ActionConfirmButton, InputElement, ConfirmDialog } from "./BaseElements.jsx"; -import { selectHideReadApprovals } from "../../reducers/configReducer.js"; +import { selectHideReadApprovals } from "../../redux/reducer/configReducer.js"; export const ApproveButton = ({ registration, ...props }) => { const queryClient = useQueryClient(); diff --git a/src/components/Content/PostThumb.jsx b/src/components/Content/PostThumb.jsx index 8e5335d..e649386 100644 --- a/src/components/Content/PostThumb.jsx +++ b/src/components/Content/PostThumb.jsx @@ -12,7 +12,7 @@ import LaunchIcon from "@mui/icons-material/Launch"; import { SanitizedLink } from "../Display.jsx"; import { Image, Video } from "./Image.jsx"; -import { selectBlurNsfw, selectNsfwWords } from "../../reducers/configReducer"; +import { selectBlurNsfw, selectNsfwWords } from "../../redux/reducer/configReducer"; function ThumbWrapper({ width = 200, tooltip, modal = null, children }) { return ( diff --git a/src/components/Filters.jsx b/src/components/Filters.jsx index e58c568..7c75d79 100644 --- a/src/components/Filters.jsx +++ b/src/components/Filters.jsx @@ -10,7 +10,7 @@ import Checkbox from "@mui/joy/Checkbox"; import Chip from "@mui/joy/Chip"; -import { selectFilterCommunity, selectFilterType, selectHideReadApprovals, selectModLogType, selectShowRemoved, selectShowResolved, setConfigItem } from "../reducers/configReducer"; +import { selectFilterCommunity, selectFilterType, selectHideReadApprovals, selectModLogType, selectShowRemoved, selectShowResolved, setConfigItem } from "../redux/reducer/configReducer"; import { getSiteData } from "../hooks/getSiteData"; import { getModLogTypeNames } from "../utils"; diff --git a/src/components/Header/AccountMenu.jsx b/src/components/Header/AccountMenu.jsx index 1972856..f91e11b 100644 --- a/src/components/Header/AccountMenu.jsx +++ b/src/components/Header/AccountMenu.jsx @@ -7,9 +7,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { LemmyHttp } from "lemmy-js-client"; import { Toaster, toast } from "sonner"; -import Button from "@mui/joy/Button"; import Menu from "@mui/joy/Menu"; -import MenuList from "@mui/joy/MenuList"; import MenuButton from "@mui/joy/MenuButton"; import Dropdown from "@mui/joy/Dropdown"; import MenuItem from "@mui/joy/MenuItem"; @@ -19,10 +17,9 @@ import ListItemContent from "@mui/joy/ListItemContent"; // 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 ArrowDropDown from "@mui/icons-material/ArrowDropDown"; -import { logoutCurrent, selectUsers } from "../../reducers/accountReducer"; +import { logoutCurrent, selectUsers } from "../../redux/reducer/accountReducer"; import { getSiteData } from "../../hooks/getSiteData"; @@ -31,7 +28,7 @@ import { BasicInfoTooltip } from "../Tooltip.jsx"; import { parseActorId, getUserRole } from "../../utils.js"; -import { setAccountIsLoading, setCurrentUser } from "../../reducers/accountReducer"; +import { setAccountIsLoading, setCurrentUser } from "../../redux/reducer/accountReducer"; import { RoleIcons } from "../Shared/Icons.jsx"; import { Typography } from "@mui/material"; @@ -78,7 +75,7 @@ function UserListItem({ user }) { } // 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)); + dispatch(setCurrentUser({base: user.base, jwt: user.jwt, site: getSite})); } catch (e) { toast(typeof e == "string" ? e : e.message); } finally { @@ -125,16 +122,11 @@ export default function AccountMenu() { startDecorator={} endDecorator={} sx={{ - mx: 1, + mx: 1, // margin on both sides of the button borderRadius: 4, - - // fontSize: "14px", - // overflow: "hidden", display: "flex", - // flexDirection: "row", alignItems: "center", justifyContent: "center", - // gap: 1, }} > diff --git a/src/components/Header/ConfigModal.jsx b/src/components/Header/ConfigModal.jsx index 8a0dae9..bf7a0c9 100644 --- a/src/components/Header/ConfigModal.jsx +++ b/src/components/Header/ConfigModal.jsx @@ -17,11 +17,10 @@ import Divider from "@mui/joy/Divider"; import { selectMandatoryModComment, setConfigItem, - setConfigItemJson, selectBlurNsfw, selectShowAvatars, selectNsfwWords, -} from "../../reducers/configReducer"; +} from "../../redux/reducer/configReducer"; function BooleanSetting({ label, subtext, value, onChange }) { return ( @@ -133,7 +132,7 @@ export default function ConfigModal({ open, onClose }) { label="NSFW Words List" subtext="list of words to also mark as NSFW" value={nsfwWords} - onChange={(e) => dispatch(setConfigItemJson("nsfwWords", e))} + onChange={(e) => dispatch(setConfigItem("nsfwWords", e))} /> )} diff --git a/src/components/Header/NotificationMenu.jsx b/src/components/Header/NotificationMenu.jsx new file mode 100644 index 0000000..a30f7ec --- /dev/null +++ b/src/components/Header/NotificationMenu.jsx @@ -0,0 +1,134 @@ +import React from "react"; + +import { useNavigate, useLocation } from "react-router-dom"; + +import IconButton from "@mui/joy/IconButton"; +import Chip from "@mui/joy/Chip"; +import Menu from "@mui/joy/Menu"; +import MenuButton from "@mui/joy/MenuButton"; +import Dropdown from "@mui/joy/Dropdown"; +import MenuItem from "@mui/joy/MenuItem"; +import ListItemDecorator from "@mui/joy/ListItemDecorator"; +import ListItemContent from "@mui/joy/ListItemContent"; +import Badge from "@mui/joy/Badge"; + +import NotificationsIcon from "@mui/icons-material/Notifications"; +import ChatIcon from "@mui/icons-material/Chat"; + +import { useLemmyHttp } from "../../hooks/useLemmyHttp"; +import { getSiteData } from "../../hooks/getSiteData"; + +import { BasicInfoTooltip } from "../Tooltip.jsx"; + +export default function NotificationMenu() { + const location = useLocation(); + const navigate = useNavigate(); + + const { localUser, localPerson, userRole } = getSiteData(); + + const { + isLoading: unreadCountLoading, + isFetching: unreadCountFetching, + error: unreadCountError, + data: unreadCountData, + } = useLemmyHttp("getUnreadCount"); + + const headerUnreadCount = React.useMemo(() => { + if (!unreadCountData) return null; + + console.log("unreadCountData", unreadCountData); + // return unreadCountData.replies + unreadCountData.mentions + unreadCountData.private_messages; + return unreadCountData.private_messages; // TODO we only show pms for now + }, [unreadCountData]); + + return ( + + + + { + // navigate("/messages"); + // }} + sx={{ + mr: 1, + // p: "2px", + borderRadius: 4, + display: "flex", + alignItems: "center", + justifyContent: "center", + }} + > + + + + + + + { + navigate("/messages"); + }} + > + + + + Messages + + {unreadCountData && unreadCountData.private_messages} + + + {/* + + + + + Replies + + {unreadCountData && unreadCountData.replies} + + + + + + + + Mentions + + {unreadCountData && unreadCountData.mentions} + + */} + + + ); +} diff --git a/src/components/Header/SiteMenu.jsx b/src/components/Header/SiteMenu.jsx index b166c2c..2d39ee8 100644 --- a/src/components/Header/SiteMenu.jsx +++ b/src/components/Header/SiteMenu.jsx @@ -15,12 +15,28 @@ import { getSiteData } from "../../hooks/getSiteData"; import { BasicInfoTooltip } from "../Tooltip.jsx"; +import { ContentIcons } from "../Shared/Icons.jsx"; + export default function SiteMenu() { const location = useLocation(); const navigate = useNavigate(); const { baseUrl, siteData, localPerson, userRole } = getSiteData(); + const { + isLoading: unreadCountLoading, + isFetching: unreadCountFetching, + error: unreadCountError, + data: unreadCountData, + } = useLemmyHttp("getUnreadCount"); + + const headerUnreadCount = React.useMemo(() => { + if (!unreadCountData) return null; + + console.log("unreadCountData", unreadCountData); + return unreadCountData.replies + unreadCountData.mentions + unreadCountData.private_messages; + }, [unreadCountData]); + const { isLoading: reportCountsLoading, isFetching: reportCountsFetching, @@ -75,6 +91,36 @@ export default function SiteMenu() { + {/* + + + */} {userRole != "user" && ( diff --git a/src/components/Header/UserMenu.jsx b/src/components/Header/UserMenu.jsx index fdec2f6..1889ca9 100644 --- a/src/components/Header/UserMenu.jsx +++ b/src/components/Header/UserMenu.jsx @@ -1,23 +1,30 @@ import React from "react"; +import { useNavigate, useLocation } from "react-router-dom"; + import { useDispatch, useSelector } from "react-redux"; import { useQueryClient, useIsFetching } from "@tanstack/react-query"; +import Chip from "@mui/joy/Chip"; import Button from "@mui/joy/Button"; import IconButton from "@mui/joy/IconButton"; import CircularProgress from "@mui/joy/CircularProgress"; +import Badge from "@mui/joy/Badge"; import CachedIcon from "@mui/icons-material/Cached"; import LogoutIcon from "@mui/icons-material/Logout"; -import ArrowDropDown from "@mui/icons-material/ArrowDropDown"; +import DashboardIcon from "@mui/icons-material/Dashboard"; +import FlagIcon from "@mui/icons-material/Flag"; +import HowToRegIcon from "@mui/icons-material/HowToReg"; +import NotificationsIcon from "@mui/icons-material/Notifications"; // 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 { logoutCurrent, selectUsers } from "../../redux/reducer/accountReducer"; import { useLemmyHttp, refreshAllData } from "../../hooks/useLemmyHttp"; import { getSiteData } from "../../hooks/getSiteData"; @@ -26,10 +33,14 @@ import { BasicInfoTooltip } from "../Tooltip.jsx"; import { parseActorId } from "../../utils.js"; import AccountMenu from "./AccountMenu.jsx"; +import NotificationMenu from "./NotificationMenu.jsx"; -import { selectAccountIsLoading } from "../../reducers/accountReducer"; +import { selectAccountIsLoading } from "../../redux/reducer/accountReducer"; export default function UserMenu() { + const location = useLocation(); + const navigate = useNavigate(); + const dispatch = useDispatch(); const queryClient = useQueryClient(); @@ -74,6 +85,8 @@ export default function UserMenu() { return ( <> + + { + if (pmReadIsSuccess) { + queryClient.invalidateQueries({ queryKey: ["lemmyHttp", localPerson.id, "getPrivateMessages"] }); + queryClient.invalidateQueries({ queryKey: ["lemmyHttp", localPerson.id, "getUnreadCount"] }); + } + }, [pmReadData]); + + return ( + + + + + + + + + + { + // pmReadCallAction({ + // private_message_id: private_message.id, + // read: !private_message.read, + // }); + // } + // } + sx={{ + position: "relative", + // cursor: messageIsMine ? "default" : "pointer", + p: 1, + m: 0, + }} + > + + {!messageIsMine && ( + + )} + + } + placement={messageIsMine ? "top-start" : "top-end"} + variant="soft" + > + + {private_message.content} + + + + + + + + + + ); +} diff --git a/src/components/Messages/PMSheet.jsx b/src/components/Messages/PMSheet.jsx new file mode 100644 index 0000000..052ac71 --- /dev/null +++ b/src/components/Messages/PMSheet.jsx @@ -0,0 +1,264 @@ +import React, { useState, useEffect, useRef } from "react"; + +import { useQueryClient } from "@tanstack/react-query"; + +import Box from "@mui/joy/Box"; +import Sheet from "@mui/joy/Sheet"; +import Divider from "@mui/joy/Divider"; +import List from "@mui/joy/List"; +import Button from "@mui/joy/Button"; +import Textarea from "@mui/joy/Textarea"; +import Typography from "@mui/joy/Typography"; +import IconButton from "@mui/joy/IconButton"; + +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; + +import { getSiteData } from "../../hooks/getSiteData"; +import { useLemmyHttpAction } from "../../hooks/useLemmyHttp"; + +import { PersonMetaTitle, PersonMetaLine, CommunityMetaLine } from "../Shared/ActorMeta.jsx"; + +import ChatMessage from "./ChatMessage.jsx"; + +export default function PMSheet({ selectedChat }) { + const queryClient = useQueryClient(); + const { baseUrl, siteData, localPerson, userRole } = getSiteData(); + + const messagesListRef = useRef(null); + + const [message, setMessage] = useState(""); + const [userId, setUserId] = useState(null); + + const { + isLoading: sendPMIsLoading, + isSuccess: sendPMIsSuccess, + data: sendPMData, + error: sendPMError, + callAction: sendPMCallAction, + } = useLemmyHttpAction("createPrivateMessage"); + + const onMessageChange = (e) => { + setMessage(e.target.value); + }; + + const handleSendMessage = async () => { + console.log("response", selectedChat, message); + + if (message == "") return; + + sendPMCallAction({ + recipient_id: selectedChat.person.id, + content: message, + }); + + setMessage(""); + }; + + // invalidate pms on send success + React.useEffect(() => { + if (sendPMIsSuccess) { + setUserId(null); // force scroll to bottom + queryClient.invalidateQueries({ queryKey: ["lemmyHttp", localPerson.id, "getPrivateMessages"] }); + } + }, [sendPMData]); + + // scroll to bottom when a new user is loaded + const scrollToBottom = () => { + // messagesListRef.current?.scrollIntoView({ + // block: "nearest", + // inline: "center", + // behavior: "smooth", + // alignToTop: false, + // }); + messagesListRef.current?.scrollIntoView(false); + }; + + // TODO this seems a bit hacky, but it works to scroll the chat down to the bottom + useEffect(() => { + const timer = setTimeout(() => { + if (selectedChat !== null) { + // if the user is not the currently loaded one + if (selectedChat?.person?.id !== userId) { + console.log("selectedChat changed userid", userId, selectedChat?.person?.id); + scrollToBottom(); + setUserId(selectedChat?.person?.id); + } + } + }, 1); + + return () => clearTimeout(timer); + }, [selectedChat]); + + if (!selectedChat) { + return ( + + + No conversation selected, phoose select one + + ); + } + + return ( + + + + + + + + + + {selectedChat.messages.map((message, index) => ( + + ))} +
+ + + + + +