cheogram-www

created pr with cheogram-www/mass-moderate.1 on 2026-10-26T16:26:15Z · by 9e834e0d
cmds
checkout latest patchset:
ssh pr.pico.sh pull cheogram-www:mass-moderate | git am -3
checkout specific patchset revision:
ssh pr.pico.sh pull cheogram-www:mass-moderate [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh cheogram-www:mass-moderate
add comment to patch request:
ssh pr.pico.sh comment cheogram-www:mass-moderate "lgtm!"
+1 -1 src/components/Chat.test.tsx #
......@@ -47,7 +47,7 @@ it("reports an empty typing string when the composer is cleared", async () => {
4747 client: mock<Client>(),
4848 getChatExtras: () => undefined,
4949 setChatExtras: vi.fn(),
50- innerChatRef: { current: { addMention: vi.fn() } },
50+ innerChatRef: { current: { addMention: vi.fn(), moderateRecent: vi.fn() } },
5151 isInvite: false,
5252 persistence: mock<Persistence & persistence.KeyValueStore>({
5353 get: async () => null,
+27 -1 src/components/Chat.tsx #
......@@ -149,6 +149,7 @@ export const ChatInner = () => {
149149 setPopupMember: (member: borogove.Member) => void;
150150 innerChatRef: MutableRefObject<{
151151 addMention: (member: borogove.Member) => void;
152+ moderateRecent: (member: borogove.Member, reason: string) => void;
152153 }>;
153154 isInvite: boolean;
154155 persistence: borogove.persistence.KeyValueStore & borogove.Persistence;
......@@ -681,9 +682,34 @@ export const ChatInner = () => {
681682 [inputRef],
682683 );
683684
685+ const moderateRecent = useCallback(
686+ async (member: borogove.Member, reason: string) => {
687+ for (const message of messages) {
688+ if (message.senderId === member.id) {
689+ activeChat.moderate(message, reason);
690+ }
691+ }
692+
693+ // Keep paging back through history until a page has no messages from
694+ // this member, so older messages they sent are moderated too.
695+ let before: borogove.ChatMessage | null = messages[0] ?? null;
696+ while (true) {
697+ const page = await activeChat.getMessagesBefore(before);
698+ const fromMember = page.filter((m) => m.senderId === member.id);
699+ for (const message of fromMember) {
700+ activeChat.moderate(message, reason);
701+ }
702+ if (fromMember.length === 0) break;
703+ before = page[0];
704+ }
705+ },
706+ [activeChat, messages],
707+ );
708+
684709 useEffect(() => {
685710 innerChatRef.current.addMention = addMention;
686- }, [innerChatRef, addMention]);
711+ innerChatRef.current.moderateRecent = moderateRecent;
712+ }, [innerChatRef, addMention, moderateRecent]);
687713
688714 const toggleDetails = useCallback(() => {
689715 if (location.pathname.endsWith("/details")) {
+61 -4 src/components/Chats.tsx #
......@@ -1,14 +1,22 @@
11 import * as borogove from "borogove";
22 import Fuse from "fuse.js";
33 import {
4+ Button as MuiButton,
5+ Checkbox,
46 Chip,
57 CircularProgress,
8+ Dialog,
9+ DialogActions,
10+ DialogContent,
11+ DialogContentText,
12+ DialogTitle,
613 Fab,
14+ FormControlLabel,
715 Menu,
816 MenuItem,
9- Dialog,
1017 ListItemIcon,
1118 ListSubheader,
19+ TextField,
1220 useMediaQuery,
1321 } from "@mui/material";
1422 import { QRCanvas, frontalCamera } from "qr/dom.js";
......@@ -88,7 +96,8 @@ export const Chats = () => {
8896
8997 const innerChatRef = useRef<{
9098 addMention: (member: borogove.Member) => void;
91- }>({ addMention: () => {} });
99+ moderateRecent: (member: borogove.Member, reason: string) => void;
100+ }>({ addMention: () => {}, moderateRecent: () => {} });
92101
93102 const [typingMembers, setTypingMembers] = useState<
94103 Record<string, borogove.Member[]>
......@@ -176,6 +185,7 @@ export const Chats = () => {
176185 const [addRoleAnchorEl, setAddRoleAnchorEl] = useState<null | HTMLElement>(
177186 null,
178187 );
188+ const [banMember, setBanMember] = useState<borogove.Member | null>(null);
179189
180190 const [popupMember, setPopupMember] = useState<borogove.Member | undefined>();
181191 const [scanQRCode, setScanQRCode] = useState(false);
......@@ -228,6 +238,21 @@ export const Chats = () => {
228238 return chat;
229239 };
230240
241+ const confirmBan = (event: React.FormEvent<HTMLFormElement>) => {
242+ event.preventDefault();
243+ if (!banMember || !activeChat) return;
244+ const data = new FormData(event.currentTarget);
245+ const reason = String(data.get("reason") ?? "").trim();
246+ setBanMember(null);
247+ const role = activeChat
248+ .availableRoles(banMember)
249+ .find((r) => r.id === "outcast");
250+ if (role) activeChat.addRole(banMember, role);
251+ if (data.get("moderate") !== null) {
252+ innerChatRef.current.moderateRecent(banMember, reason);
253+ }
254+ };
255+
231256 const conversationInfo = (c: borogove.Chat) => {
232257 let info: string | JSX.Element = c.preview();
233258 const extras = getChatExtras(c.chatId);
......@@ -1124,9 +1149,13 @@ export const Chats = () => {
11241149 <MenuItem
11251150 key={role.id}
11261151 onClick={() => {
1127- activeChat?.addRole(popupMember, role);
1128- setPopupMember(undefined);
11291152 setAddRoleAnchorEl(null);
1153+ setPopupMember(undefined);
1154+ if (role.id === "outcast") {
1155+ setBanMember(popupMember);
1156+ return;
1157+ }
1158+ activeChat?.addRole(popupMember, role);
11301159 }}
11311160 >
11321161 {role.title}
......@@ -1170,6 +1199,34 @@ export const Chats = () => {
11701199 </Dialog>
11711200 )}
11721201
1202+ {banMember && (
1203+ <Dialog open onClose={() => setBanMember(null)} id="ban-dialog">
1204+ <DialogTitle>Ban {banMember.displayName}?</DialogTitle>
1205+ <form onSubmit={confirmBan}>
1206+ <DialogContent>
1207+ <DialogContentText>
1208+ They will be removed from this channel and unable to rejoin.
1209+ </DialogContentText>
1210+ <FormControlLabel
1211+ control={<Checkbox name="moderate" defaultChecked />}
1212+ label="Also moderate their recent messages"
1213+ />
1214+ <TextField
1215+ name="reason"
1216+ label="Reason"
1217+ defaultValue="Spam"
1218+ fullWidth
1219+ margin="dense"
1220+ />
1221+ </DialogContent>
1222+ <DialogActions>
1223+ <MuiButton onClick={() => setBanMember(null)}>Cancel</MuiButton>
1224+ <MuiButton type="submit">Ban</MuiButton>
1225+ </DialogActions>
1226+ </form>
1227+ </Dialog>
1228+ )}
1229+
11731230 <Dialog
11741231 open={scanQRCode}
11751232 onClose={() => setScanQRCode(false)}
Back to top