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 {