Skip to content

Commit e8e49d1

Browse files
authored
feat(mod): ban indicator on profile + flagged user intake tab (#2585)
1 parent 09f2c7e commit e8e49d1

File tree

9 files changed

+807
-370
lines changed

9 files changed

+807
-370
lines changed

react_main/src/pages/Community/UserSearch.jsx

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import React, { useState, useEffect, useContext } from "react";
1+
import React, { useState, useEffect } from "react";
22
import axios from "axios";
3-
import { UserContext } from "../../Contexts";
43
import { useErrorAlert } from "../../components/Alerts";
54
import { NameWithAvatar, StatusIcon } from "../User/User";
65
import { getPageNavFilterArg, PageNav } from "../../components/Nav";
@@ -20,8 +19,6 @@ export default function UserSearch(props) {
2019
const [userList, setUserList] = useState([]);
2120
const [searchVal, setSearchVal] = useState("");
2221

23-
const user = useContext(UserContext);
24-
2522
useEffect(() => {
2623
document.title = "Users | UltiMafia";
2724
}, []);
@@ -100,7 +97,6 @@ export default function UserSearch(props) {
10097
</Grid>
10198
<Grid item xs={12} md={3}>
10299
<NewestUsers />
103-
{user.perms.viewFlagged && <FlaggedUsers />}
104100
</Grid>
105101
</Grid>
106102
</Box>
@@ -174,63 +170,3 @@ function NewestUsers(props) {
174170
);
175171
}
176172

177-
function FlaggedUsers(props) {
178-
const [page, setPage] = useState(1);
179-
const [users, setUsers] = useState([]);
180-
181-
const errorAlert = useErrorAlert();
182-
183-
useEffect(() => {
184-
onPageNav(1);
185-
}, []);
186-
187-
function onPageNav(_page) {
188-
var filterArg = getPageNavFilterArg(_page, page, users, "joined");
189-
190-
if (filterArg == null) return;
191-
192-
axios
193-
.get(`/api/user/flagged?${filterArg}`)
194-
.then((res) => {
195-
if (res.data.length > 0) {
196-
setUsers(res.data);
197-
setPage(_page);
198-
}
199-
})
200-
.catch(errorAlert);
201-
}
202-
203-
const userRows = users.map((user) => (
204-
<Card
205-
key={user.id}
206-
className="user-row"
207-
variant="outlined"
208-
sx={{ marginBottom: 2 }}
209-
>
210-
<CardContent sx={{ display: "flex", flexDirection: "column" }}>
211-
<NameWithAvatar
212-
id={user.id}
213-
name={user.name}
214-
avatar={user.avatar}
215-
vanityUrl={user.vanityUrl}
216-
/>
217-
<Typography variant="caption" sx={{ marginTop: "4px" }}>
218-
<Time minSec millisec={Date.now() - user.joined} suffix=" ago" />
219-
</Typography>
220-
</CardContent>
221-
</Card>
222-
));
223-
224-
return (
225-
<Box className="flagged-users box-panel">
226-
<Typography variant="h4" className="heading">
227-
Flagged Users
228-
</Typography>
229-
<Box className="users-list">
230-
<PageNav page={page} onNav={onPageNav} inverted />
231-
{userRows}
232-
<PageNav page={page} onNav={onPageNav} inverted />
233-
</Box>
234-
</Box>
235-
);
236-
}

react_main/src/pages/Policy/Moderation.jsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ function getTabValue(pathname) {
123123
if (pathname.includes("/reports")) return "reports";
124124
if (pathname.includes("/competitive")) return "competitive";
125125
if (pathname.includes("/handbook")) return "handbook";
126+
if (pathname.includes("/flagged-intake")) return "flagged-intake";
126127
return "log";
127128
}
128129

@@ -149,15 +150,21 @@ export default function Moderation() {
149150
!user?.perms?.manageCompetitive
150151
) {
151152
navigate("/policy/moderation", { replace: true });
153+
} else if (
154+
location.pathname.includes("/flagged-intake") &&
155+
!user?.perms?.whitelist
156+
) {
157+
navigate("/policy/moderation", { replace: true });
152158
}
153-
}, [location.pathname, user?.perms?.viewModActions, user?.perms?.manageCompetitive, navigate]);
159+
}, [location.pathname, user?.perms?.viewModActions, user?.perms?.manageCompetitive, user?.perms?.whitelist, navigate]);
154160

155161
const handleTabChange = (_, newValue) => {
156162
const base = "/policy/moderation";
157163
if (newValue === "log") navigate(base);
158164
else if (newValue === "reports") navigate(`${base}/reports`);
159165
else if (newValue === "competitive") navigate(`${base}/competitive`);
160166
else if (newValue === "handbook") navigate(`${base}/handbook`);
167+
else if (newValue === "flagged-intake") navigate(`${base}/flagged-intake`);
161168
};
162169

163170
return (
@@ -175,6 +182,9 @@ export default function Moderation() {
175182
{user?.perms?.manageCompetitive && (
176183
<Tab label="Competitive Management" value="competitive" />
177184
)}
185+
{user?.perms?.whitelist && (
186+
<Tab label="Flagged Intake" value="flagged-intake" />
187+
)}
178188
</Tabs>
179189

180190
<Outlet
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import React, { useState, useEffect, useContext } from "react";
2+
import { useOutletContext } from "react-router-dom";
3+
import axios from "axios";
4+
import {
5+
Box,
6+
Card,
7+
CardContent,
8+
IconButton,
9+
Stack,
10+
Tooltip,
11+
Typography,
12+
} from "@mui/material";
13+
14+
import { UserContext } from "Contexts";
15+
import { useErrorAlert } from "components/Alerts";
16+
import { NameWithAvatar } from "pages/User/User";
17+
import { Time } from "components/Basic";
18+
import { PageNav } from "components/Nav";
19+
import { Loading } from "components/Loading";
20+
21+
export default function FlaggedIntake() {
22+
const { user } = useOutletContext() || {};
23+
const currentUser = useContext(UserContext);
24+
const activeUser = user || currentUser;
25+
26+
const [users, setUsers] = useState([]);
27+
const [page, setPage] = useState(1);
28+
const [loading, setLoading] = useState(true);
29+
const [actionLoading, setActionLoading] = useState({});
30+
31+
const errorAlert = useErrorAlert();
32+
33+
const canWhitelist = activeUser?.perms?.whitelist;
34+
35+
useEffect(() => {
36+
loadFlaggedUsers(1);
37+
}, []);
38+
39+
function loadFlaggedUsers(targetPage) {
40+
setLoading(true);
41+
42+
const params = targetPage === 1 ? "" : `?last=${users[users.length - 1]?.joined || ""}`;
43+
44+
axios
45+
.get(`/api/user/flagged${params}`)
46+
.then((res) => {
47+
if (res.data.length > 0 || targetPage === 1) {
48+
setUsers(res.data);
49+
setPage(targetPage);
50+
}
51+
setLoading(false);
52+
})
53+
.catch((e) => {
54+
errorAlert(e);
55+
setLoading(false);
56+
});
57+
}
58+
59+
function onPageNav(newPage) {
60+
if (newPage === page) return;
61+
62+
if (newPage > page && users.length > 0) {
63+
const lastJoined = users[users.length - 1]?.joined;
64+
axios
65+
.get(`/api/user/flagged?last=${lastJoined}`)
66+
.then((res) => {
67+
if (res.data.length > 0) {
68+
setUsers(res.data);
69+
setPage(newPage);
70+
}
71+
})
72+
.catch(errorAlert);
73+
} else if (newPage < page && users.length > 0) {
74+
const firstJoined = users[0]?.joined;
75+
axios
76+
.get(`/api/user/flagged?first=${firstJoined}`)
77+
.then((res) => {
78+
if (res.data.length > 0) {
79+
setUsers(res.data);
80+
setPage(newPage);
81+
}
82+
})
83+
.catch(errorAlert);
84+
}
85+
}
86+
87+
function onWhitelist(userId) {
88+
if (!canWhitelist) return;
89+
90+
setActionLoading((prev) => ({ ...prev, [userId]: "whitelist" }));
91+
92+
axios
93+
.post("/api/mod/whitelist", { userId })
94+
.then(() => {
95+
setUsers((prev) => prev.filter((u) => u.id !== userId));
96+
setActionLoading((prev) => {
97+
const next = { ...prev };
98+
delete next[userId];
99+
return next;
100+
});
101+
})
102+
.catch((e) => {
103+
errorAlert(e);
104+
setActionLoading((prev) => {
105+
const next = { ...prev };
106+
delete next[userId];
107+
return next;
108+
});
109+
});
110+
}
111+
112+
function onBlacklist(userId) {
113+
if (!canWhitelist) return;
114+
115+
setActionLoading((prev) => ({ ...prev, [userId]: "blacklist" }));
116+
117+
axios
118+
.post("/api/mod/blacklist", { userId })
119+
.then(() => {
120+
setUsers((prev) => prev.filter((u) => u.id !== userId));
121+
setActionLoading((prev) => {
122+
const next = { ...prev };
123+
delete next[userId];
124+
return next;
125+
});
126+
})
127+
.catch((e) => {
128+
errorAlert(e);
129+
setActionLoading((prev) => {
130+
const next = { ...prev };
131+
delete next[userId];
132+
return next;
133+
});
134+
});
135+
}
136+
137+
if (loading && users.length === 0) {
138+
return <Loading small />;
139+
}
140+
141+
const userRows = users.map((flaggedUser) => {
142+
const isProcessing = actionLoading[flaggedUser.id];
143+
144+
return (
145+
<Card
146+
key={flaggedUser.id}
147+
variant="outlined"
148+
sx={{ mb: 1 }}
149+
>
150+
<CardContent
151+
sx={{
152+
display: "flex",
153+
alignItems: "center",
154+
justifyContent: "space-between",
155+
py: 1.5,
156+
"&:last-child": { pb: 1.5 },
157+
}}
158+
>
159+
<Stack direction="row" spacing={2} sx={{ alignItems: "center", flexGrow: 1 }}>
160+
<Box sx={{ minWidth: 150 }}>
161+
<NameWithAvatar
162+
id={flaggedUser.id}
163+
name={flaggedUser.name}
164+
avatar={flaggedUser.avatar}
165+
/>
166+
</Box>
167+
<Stack direction="column" spacing={0.5}>
168+
<Typography variant="caption" color="textSecondary">
169+
Joined: {new Date(flaggedUser.joined).toLocaleDateString()}
170+
{" ("}
171+
<Time minSec millisec={Date.now() - flaggedUser.joined} suffix=" ago" />
172+
{")"}
173+
</Typography>
174+
</Stack>
175+
</Stack>
176+
177+
{canWhitelist && (
178+
<Stack direction="row" spacing={1}>
179+
<Tooltip title="Whitelist (approve user)">
180+
<span>
181+
<IconButton
182+
size="small"
183+
color="success"
184+
onClick={() => onWhitelist(flaggedUser.id)}
185+
disabled={!!isProcessing}
186+
sx={{
187+
border: "1px solid",
188+
borderColor: "success.main",
189+
"&:hover": {
190+
backgroundColor: "success.main",
191+
color: "white",
192+
},
193+
}}
194+
>
195+
<i
196+
className={isProcessing === "whitelist" ? "fas fa-spinner fa-spin" : "fas fa-check"}
197+
/>
198+
</IconButton>
199+
</span>
200+
</Tooltip>
201+
<Tooltip title="Blacklist (permanent IP ban)">
202+
<span>
203+
<IconButton
204+
size="small"
205+
color="error"
206+
onClick={() => onBlacklist(flaggedUser.id)}
207+
disabled={!!isProcessing}
208+
sx={{
209+
border: "1px solid",
210+
borderColor: "error.main",
211+
"&:hover": {
212+
backgroundColor: "error.main",
213+
color: "white",
214+
},
215+
}}
216+
>
217+
<i
218+
className={isProcessing === "blacklist" ? "fas fa-spinner fa-spin" : "fas fa-times"}
219+
/>
220+
</IconButton>
221+
</span>
222+
</Tooltip>
223+
</Stack>
224+
)}
225+
</CardContent>
226+
</Card>
227+
);
228+
});
229+
230+
return (
231+
<Box className="flagged-intake">
232+
<Typography variant="h4" sx={{ mb: 2 }}>
233+
Flagged User Intake
234+
</Typography>
235+
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
236+
Review flagged users and decide whether to whitelist (approve) or blacklist (permanently ban) them.
237+
</Typography>
238+
239+
<PageNav page={page} onNav={onPageNav} inverted />
240+
241+
{users.length === 0 ? (
242+
<Typography variant="body1" sx={{ py: 2, textAlign: "center" }}>
243+
No flagged users to review.
244+
</Typography>
245+
) : (
246+
<Box sx={{ my: 1 }}>{userRows}</Box>
247+
)}
248+
249+
<PageNav page={page} onNav={onPageNav} inverted />
250+
</Box>
251+
);
252+
}

react_main/src/pages/Policy/Policy.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ModerationLog } from "./Moderation";
1010
import Reports from "./Reports";
1111
import CompetitiveManagement from "./Moderation/CompetitiveManagement";
1212
import StaffHandbook from "./Moderation/StaffHandbook";
13+
import FlaggedIntake from "./Moderation/FlaggedIntake";
1314

1415
function ReportsRedirect() {
1516
const location = useLocation();
@@ -41,6 +42,7 @@ export default function Policy() {
4142
/>
4243
<Route path="competitive" element={<CompetitiveManagement />} />
4344
<Route path="handbook" element={<StaffHandbook />} />
45+
<Route path="flagged-intake" element={<FlaggedIntake />} />
4446
</Route>
4547
<Route path="reports/*" element={<ReportsRedirect />} />
4648
<Route path="*" element={<Navigate to="rules" />} />

0 commit comments

Comments
 (0)