diff --git a/Dockerfile b/Dockerfile index ebe9329..ac3a28a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,8 +12,15 @@ RUN npm run build # copy files to run container FROM nginx:stable-alpine + +# copy distribution from build step COPY --from=build /app/dist /usr/share/nginx/html -# expose and start nginx +# install entrypoint script to /usr/local/bin/ +RUN mkdir -p /usr/local/bin/ +COPY ./scripts/docker-startup.sh /usr/local/bin/docker-startup.sh +RUN chmod 700 /usr/local/bin/docker-startup.sh + +# expose and set entrypoint EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +ENTRYPOINT ["sh", "/usr/local/bin/docker-startup.sh"] diff --git a/README.md b/README.md index 49a1a95..40f029d 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,13 @@ - Filter by local instance actions - Quick Switch Accounts on different Lemmy instances +## User Types + +There are 3 types of users in Lemmy Modder: user, mod and `admin` + +This is determined based on the amount of moderated communities you manage. + + ## Hosting Options @@ -49,9 +56,32 @@ services: restart: unless-stopped ports: - 9696:80 + environment: + LOCK_DOMAIN: modder.example.com # optionally locks the domain that can be used with this instance ``` 2. Bring up the new container `docker-compose up -d lemmy-modder` -2. Setup your reverse proxy to proxy requests for `modder.example.com` to the new container on port `80`. +3. Setup your reverse proxy to proxy requests for `modder.example.com` to the new container on port `80`. + +If you use Traefik, the labels will be something like this: +```yaml + networks: + - traefik-net + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik-net" + - "traefik.http.services.lemmy_mod.loadbalancer.server.port=80" + + # internet https + - "traefik.http.routers.lemmy_mod_https_net.rule=Host(`modder.example.com`)" + - "traefik.http.routers.lemmy_mod_https_net.entrypoints=https" + - "traefik.http.routers.lemmy_mod_https_net.tls.certResolver=SSL_RESOLVER" + + # internet http redirect + - "traefik.http.routers.lemmy_mod_http_redirect_net.rule=Host(`modder.example.com`)" + - "traefik.http.routers.lemmy_mod_http_redirect_net.entrypoints=http" + - "traefik.http.routers.lemmy_mod_http_redirect_net.middlewares=redirect_https@file" +``` + _There are no more steps, as there is no users or databases._ @@ -79,9 +109,23 @@ npm start ``` 5. Open http://localhost:9696 in your browser + +### Testing Docker Image + +1. Build the docker image +``` +docker build -t lemmy-modder:local . +``` + +2. Run the docker image _(with lock example)_ +``` +docker run --rm --env LOCK_DOMAIN="lemmy.tgxn.net" -p 9696:80 lemmy-modder:local +``` + + # Credits -Lemmy Devs https://github.com/LemmyNet +Lemmy Devs https://github.com/LemmyNet Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license. diff --git a/index.html b/index.html index 40f7036..7c4791d 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,8 @@ + + diff --git a/package.json b/package.json index cfd2aad..e53ec6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-modder-frontend", - "version": "1.2.1", + "version": "1.2.2", "description": "Lemmy Moderation App", "author": "tgxn", "license": "MIT", diff --git a/scripts/docker-startup.sh b/scripts/docker-startup.sh new file mode 100644 index 0000000..f425a18 --- /dev/null +++ b/scripts/docker-startup.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# load runtime configuration +LOCK_DOMAIN=${LOCK_DOMAIN:-""} + +# store runtime configuration +RUNTIME_CONFIG_FILE=/usr/share/nginx/html/runtime-config.js +if [ -f $RUNTIME_CONFIG_FILE ]; then + rm $RUNTIME_CONFIG_FILE +fi + +echo "$(cat < $RUNTIME_CONFIG_FILE + +# Start Nginx (default entrypoint) +exec nginx -g "daemon off;" diff --git a/src/components/Actions/PostButtons.jsx b/src/components/Actions/PostButtons.jsx index f128941..da7ed34 100644 --- a/src/components/Actions/PostButtons.jsx +++ b/src/components/Actions/PostButtons.jsx @@ -3,6 +3,8 @@ import { useSelector } from "react-redux"; import { useQueryClient } from "@tanstack/react-query"; +import Typography from "@mui/joy/Typography"; + import DoneAllIcon from "@mui/icons-material/DoneAll"; import { useLemmyHttpAction } from "../../hooks/useLemmyHttp.js"; diff --git a/src/components/Actions/RegistrationButtons.jsx b/src/components/Actions/RegistrationButtons.jsx index ce99a2e..5bcce39 100644 --- a/src/components/Actions/RegistrationButtons.jsx +++ b/src/components/Actions/RegistrationButtons.jsx @@ -4,6 +4,8 @@ import { toast } from "sonner"; import { useSelector } from "react-redux"; import { useQueryClient } from "@tanstack/react-query"; +import Typography from "@mui/joy/Typography"; + import ThumbUpIcon from "@mui/icons-material/ThumbUp"; import ThumbDownIcon from "@mui/icons-material/ThumbDown"; diff --git a/src/components/Activity/ModLogAccordians.jsx b/src/components/Activity/ModLogAccordians.jsx index 09c9d2e..50e2d9d 100644 --- a/src/components/Activity/ModLogAccordians.jsx +++ b/src/components/Activity/ModLogAccordians.jsx @@ -22,33 +22,33 @@ export default function ModLogAccordians({ modLogData }) { if (modLogItem.type === "removed_posts") { return ; } - if (modLogItem.type === "removed_comments") { - return ; - } - if (modLogItem.type === "banned_from_community") { - return ; - } if (modLogItem.type === "locked_posts") { return ; } - if (modLogItem.type === "banned") { - return ; - } - if (modLogItem.type === "added_to_community") { - return ; - } if (modLogItem.type === "featured_posts") { return ; } + if (modLogItem.type === "removed_comments") { + return ; + } if (modLogItem.type === "removed_communities") { return ; } + if (modLogItem.type === "banned_from_community") { + return ; + } + if (modLogItem.type === "added_to_community") { + return ; + } if (modLogItem.type === "transferred_to_community") { return ; } if (modLogItem.type === "added") { return ; } + if (modLogItem.type === "banned") { + return ; + } return ( diff --git a/src/components/Filters.jsx b/src/components/Filters.jsx index 664c0ab..6122a83 100644 --- a/src/components/Filters.jsx +++ b/src/components/Filters.jsx @@ -54,7 +54,7 @@ export function FilterCommunity() { }} > {modCommms.map((community) => { const { name, title } = community.community; diff --git a/src/components/SiteHeader.jsx b/src/components/SiteHeader.jsx index c204cd0..5c1cdde 100644 --- a/src/components/SiteHeader.jsx +++ b/src/components/SiteHeader.jsx @@ -44,7 +44,7 @@ import { logoutCurrent } from "../reducers/accountReducer"; import { LemmyHttp } from "lemmy-js-client"; -import { useLemmyHttp } from "../hooks/useLemmyHttp"; +import { useLemmyHttp, refreshAllData } from "../hooks/useLemmyHttp"; import { getSiteData } from "../hooks/getSiteData"; import { HeaderChip } from "./Display.jsx"; @@ -88,99 +88,105 @@ function SiteMenu() { return ( <> - - - - - - - + {userRole != "user" && ( + + + + )} + + {userRole == "admin" && ( + + + + )} - + @@ -222,6 +228,8 @@ function UserMenu() { const users = useSelector((state) => state.accountReducer.users); + const { mutate: refreshMutate } = refreshAllData(); + const [isLoading, setIsLoading] = React.useState(false); const { baseUrl, siteData, localPerson, userRole } = getSiteData(); @@ -268,7 +276,7 @@ function UserMenu() { borderRadius: 4, }} onClick={() => { - queryClient.invalidateQueries({ queryKey: ["lemmyHttp"] }); + refreshMutate(); }} > @@ -449,7 +457,7 @@ export default function SiteHeader({ height }) { )} - + state.accountReducer.currentUser); @@ -70,3 +72,26 @@ export function useLemmyHttpAction(callLemmyMethod) { data: mutation.data, }; } + +export function refreshAllData() { + const dispatch = useDispatch(); + const queryClient = useQueryClient(); + + const currentUser = useSelector((state) => state.accountReducer.currentUser); + + const mutation = useMutation({ + mutationFn: async () => { + const lemmyClient = new LemmyHttp(`https://${currentUser.base}`); + + const getSite = await lemmyClient.getSite({ + auth: currentUser.jwt, + }); + + dispatch(updateCurrentUserData(getSite)); + + queryClient.invalidateQueries({ queryKey: ["lemmyHttp"] }); + }, + }); + + return mutation; +} diff --git a/src/hooks/useLemmyReports.js b/src/hooks/useLemmyReports.js index 11c0810..7b1fc8b 100644 --- a/src/hooks/useLemmyReports.js +++ b/src/hooks/useLemmyReports.js @@ -82,12 +82,19 @@ export default function useLemmyReports() { } const mergedReports = useMemo(() => { - if (!postReportsData || !commentReportsData || !pmReportsData) return; - if (postReportsLoading || commentReportsLoading || pmReportsLoading) return; + console.log("mergedReports", postReportsData, commentReportsData, pmReportsData); - if (pmReportsError && pmReportsError.response.status === 400) { + // must have post and comment report data + if (!postReportsData || !commentReportsData || !(pmReportsData || userRole != "admin")) return; + + // return if either of these are still loading + if (postReportsLoading || commentReportsLoading || (pmReportsLoading && userRole === "admin")) return; + + console.log("mergedReports", postReportsData, commentReportsData, pmReportsData); + + if (!pmReportsData) { console.log("pmReportsError - may not be site admin", pmReportsError); - pmReportsData.private_message_reports = []; + // pmReportsData.private_message_reports = []; } let normalPostReports = mapPagesData(postReportsData.pages, (report) => { @@ -176,10 +183,12 @@ export default function useLemmyReports() { commentReportsData, postReportsData, pmReportsData, + postReportsLoading, + commentReportsLoading, + pmReportsLoading, filterType, filterCommunity, showResolved, - // showRemoved, ]); const isLoading = commentReportsLoading || postReportsLoading || (pmReportsLoading && userRole === "admin"); diff --git a/src/pages/Actions.jsx b/src/pages/Actions.jsx index 98cc900..751c4a0 100644 --- a/src/pages/Actions.jsx +++ b/src/pages/Actions.jsx @@ -73,6 +73,7 @@ export default function Actions() { modLogPageData.map((modLogItem) => { // extract time from the type of mod action let time; + if (modlogType === "removed_posts") time = modLogItem.mod_remove_post.when_; if (modlogType === "locked_posts") time = modLogItem.mod_lock_post.when_; if (modlogType === "featured_posts") time = modLogItem.mod_feature_post.when_; @@ -84,6 +85,11 @@ export default function Actions() { if (modlogType === "added") time = modLogItem.mod_add.when_; if (modlogType === "banned") time = modLogItem.mod_ban.when_; + if (modlogType === "admin_purged_persons") time = modLogItem.admin_purge_person.when_; + if (modlogType === "admin_purged_communities") time = modLogItem.admin_purge_community.when_; + if (modlogType === "admin_purged_posts") time = modLogItem.admin_purge_post.when_; + if (modlogType === "admin_purged_comments") time = modLogItem.admin_purge_comment.when_; + return { type: modlogType, time, @@ -96,11 +102,30 @@ export default function Actions() { allModActions = allModActions.concat(thisItems); } + // this is hard since `moderator is not visible for non-admins + // which means we'd have to extract the actor id from the object, which is different for each action + // for now they get removed when we attempt to render them if (limitLocalInstance) { allModActions = allModActions.filter((item) => { - // console.log("item", item, siteData); - if (!item.moderator) return false; - return locaUserParsedActor.actorBaseUrl === parseActorId(item.moderator.actor_id).actorBaseUrl; + // this only works for site admins + if (item.moderator) + locaUserParsedActor.actorBaseUrl === parseActorId(item.moderator.actor_id).actorBaseUrl; + + return !item.localCommunity; + + // let time; + // if (modlogType === "removed_posts") time = modLogItem.mod_remove_post.when_; + // if (modlogType === "locked_posts") time = modLogItem.mod_lock_post.when_; + // if (modlogType === "featured_posts") time = modLogItem.mod_feature_post.when_; + // if (modlogType === "removed_comments") time = modLogItem.mod_remove_comment.when_; + // if (modlogType === "removed_communities") time = modLogItem.mod_remove_community.when_; + // if (modlogType === "banned_from_community") time = modLogItem.mod_ban_from_community.when_; + // if (modlogType === "added_to_community") time = modLogItem.mod_add_community.when_; + // if (modlogType === "transferred_to_community") time = modLogItem.mod_transfer_community.when_; + // if (modlogType === "added") time = modLogItem.mod_add.when_; + // if (modlogType === "banned") time = modLogItem.mod_ban.when_; + + return false; }); } @@ -118,7 +143,7 @@ export default function Actions() { }); return allModActions; - }, [modlogData, limitLocalInstance]); + }, [modlogData, modLogType, limitLocalInstance]); // fetch next page when in view React.useEffect(() => { @@ -186,16 +211,20 @@ export default function Actions() { }} > - { - // dispatch(setConfigItem("hideReadApprovals", !hideReadApprovals)); - console.log("toggle", !limitLocalInstance); - setLimitLocalInstance(!limitLocalInstance); - }} - /> + + {/* temp. hidden because non-admins can't see the `moderator` field */} + {userRole == "admin" && ( + { + // dispatch(setConfigItem("hideReadApprovals", !hideReadApprovals)); + console.log("toggle", !limitLocalInstance); + setLimitLocalInstance(!limitLocalInstance); + }} + /> + )} - + {modlogHasNextPage && ( diff --git a/src/pages/Approvals.jsx b/src/pages/Approvals.jsx index 7c38883..0fed338 100644 --- a/src/pages/Approvals.jsx +++ b/src/pages/Approvals.jsx @@ -80,6 +80,26 @@ export default function Approvals() { } }, [inView]); + if (userRole != "admin") { + return ( + + You are not an admin! + + ); + } + if (registrationsLoading) { return ( state.accountReducer.accountIsLoading); @@ -34,7 +39,7 @@ export default function LoginForm() { const isInElectron = useSelector((state) => state.configReducer.isInElectron); // form state - const [instanceBase, setInstanceBase] = React.useState(""); + const [instanceBase, setInstanceBase] = React.useState(domainLock ? domainLock : ""); const [username, setUsername] = React.useState(""); const [password, setPassword] = React.useState(""); @@ -147,11 +152,11 @@ export default function LoginForm() { setInstanceBase(e.target.value)} + onChange={(e) => (domainLock ? null : setInstanceBase(e.target.value))} variant="outlined" color="neutral" sx={{ mb: 1, width: "100%" }} - disabled={accountIsLoading} + disabled={domainLock || accountIsLoading} /> - + {loginError && ( diff --git a/src/pages/Reports.jsx b/src/pages/Reports.jsx index b0b3071..4f9ff3d 100644 --- a/src/pages/Reports.jsx +++ b/src/pages/Reports.jsx @@ -5,6 +5,8 @@ import Button from "@mui/joy/Button"; import Sheet from "@mui/joy/Sheet"; import CircularProgress from "@mui/joy/CircularProgress"; +import SoapIcon from "@mui/icons-material/Soap"; + import { useInView } from "react-intersection-observer"; import { FilterCommunity, FilterTypeSelect, FilterResolved, FilterRemoved } from "../components/Filters"; @@ -14,6 +16,8 @@ import useLemmyReports from "../hooks/useLemmyReports"; import ReportsList from "../components/ReportsList.jsx"; +import { getSiteData } from "../hooks/getSiteData"; + export default function Reports() { // const { // isLoading: reportCountsLoading, @@ -22,6 +26,8 @@ export default function Reports() { // data: reportCountsData, // } = useLemmyHttp("getReportCount"); + const { baseUrl, siteData, localPerson, userRole } = getSiteData(); + const { ref, inView, entry } = useInView({ /* Optional options */ threshold: 0, @@ -46,6 +52,27 @@ export default function Reports() { const isLoading = loadingReports; const isError = isReportsError; + if (userRole == "user") { + return ( + + + You do not moderate any communities! + + ); + } + if (isLoading) { return ( { currentUser: action.payload, }; + // this should update the current user's `site` data + case "updateCurrentUserData": + const newCurrentUser = { + ...state.currentUser, + site: action.payload.site, + }; + localStorage.setItem("currentUser", JSON.stringify(newCurrentUser)); + return { + ...state, + currentUser: newCurrentUser, + }; + case "logoutCurrent": localStorage.removeItem("currentUser"); return {