Skip to content

Commit 932600b

Browse files
authored
fix: some follow up fixes for userspam management (#3517)
1 parent ca416fe commit 932600b

23 files changed

Lines changed: 750 additions & 280 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ jobs:
114114
run: pnpm install --frozen-lockfile
115115

116116
- name: Install Firebase CLI
117-
run: npm install -g firebase-tools@10
117+
run: pnpm install -g firebase-tools@10
118118

119119
- name: Verify Firebase CLI
120120
run: firebase --version

client/components/SpamStatusMenu/SpamStatusMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const SpamStatusMenu = (props: Props) => {
5151
aria-label="Spam actions"
5252
buttonContent="Spam"
5353
buttonProps={{
54-
icon: <Icon icon="flag" iconSize={small ? 12 : 14} />,
54+
icon: <Icon icon="shield" iconSize={small ? 12 : 14} />,
5555
minimal: true,
5656
small,
5757
loading: isLoading,

client/containers/SuperAdminDashboard/UserSpam/MarkSpamStatusButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const MarkSpamStatusButton = (props: Props) => {
4343
return (
4444
<Button
4545
minimal
46+
small
4647
loading={isLoading}
4748
onClick={handleClick}
4849
{...propsForStatuses[status]}

client/containers/SuperAdminDashboard/UserSpam/UserSpam.tsx

Lines changed: 200 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import type { SpamUserQueryOrdering } from 'types';
33
import type { SpamUsersFilter } from './filters';
44
import type { SpamUser } from './types';
55

6-
import React, { useCallback, useState } from 'react';
6+
import React, { useCallback, useMemo, useState } from 'react';
77

8-
import { HTMLSelect, Spinner } from '@blueprintjs/core';
8+
import { Button, ButtonGroup, HTMLSelect, Spinner } from '@blueprintjs/core';
99
import { useDebounce, useUpdateEffect } from 'react-use';
1010

1111
import { OverviewSearchGroup } from 'client/containers/DashboardOverview/helpers';
@@ -25,12 +25,38 @@ type Props = {
2525
const sortOptions = [
2626
{ value: 'user-created-at:DESC', label: 'Newest users' },
2727
{ value: 'user-created-at:ASC', label: 'Oldest users' },
28+
{ value: 'last-activity:DESC', label: 'Most recently active' },
29+
{ value: 'last-activity:ASC', label: 'Least recently active' },
30+
{ value: 'activity-count:DESC', label: 'Most activities' },
31+
{ value: 'activity-count:ASC', label: 'Fewest activities' },
2832
{ value: 'discussion-count:DESC', label: 'Most discussions' },
2933
{ value: 'discussion-count:ASC', label: 'Fewest discussions' },
3034
{ value: 'spam-score:DESC', label: 'Highest spam score' },
3135
{ value: 'spam-score:ASC', label: 'Lowest spam score' },
3236
];
3337

38+
type DatePreset = { label: string; days: number };
39+
40+
const datePresets: DatePreset[] = [
41+
{ label: '24h', days: 1 },
42+
{ label: '7d', days: 7 },
43+
{ label: '30d', days: 30 },
44+
{ label: '90d', days: 90 },
45+
];
46+
47+
const daysAgo = (n: number): string => {
48+
const d = new Date();
49+
d.setDate(d.getDate() - n);
50+
return d.toISOString();
51+
};
52+
53+
const toDateInputValue = (iso: string): string => iso.slice(0, 10);
54+
55+
const fromDateInputValue = (val: string): string | undefined => {
56+
if (!val) return undefined;
57+
return new Date(val).toISOString();
58+
};
59+
3460
const parseSort = (value: string): SpamUserQueryOrdering => {
3561
const [field, direction] = value.split(':');
3662
return { field, direction } as SpamUserQueryOrdering;
@@ -46,9 +72,41 @@ const UserSpam = (props: Props) => {
4672
);
4773
const [communityInput, setCommunityInput] = useState('');
4874
const [communitySubdomain, setCommunitySubdomain] = useState('');
75+
const [createdAfter, setCreatedAfter] = useState<string | undefined>();
76+
const [createdBefore, setCreatedBefore] = useState<string | undefined>();
77+
const [activeAfter, setActiveAfter] = useState<string | undefined>();
78+
const [activeBefore, setActiveBefore] = useState<string | undefined>();
79+
const [createdPreset, setCreatedPreset] = useState<number | null>(null);
80+
const [activePreset, setActivePreset] = useState<number | null>(null);
81+
const [minActivitiesInput, setMinActivitiesInput] = useState('');
82+
const [maxActivitiesInput, setMaxActivitiesInput] = useState('');
83+
const [minActivities, setMinActivities] = useState<number | undefined>();
84+
const [maxActivities, setMaxActivities] = useState<number | undefined>();
4985

5086
useDebounce(() => setSearchTerm(inputSearchTerm), 300, [inputSearchTerm]);
5187
useDebounce(() => setCommunitySubdomain(communityInput.trim()), 300, [communityInput]);
88+
useDebounce(
89+
() => setMinActivities(minActivitiesInput ? Number(minActivitiesInput) : undefined),
90+
300,
91+
[minActivitiesInput],
92+
);
93+
useDebounce(
94+
() => setMaxActivities(maxActivitiesInput ? Number(maxActivitiesInput) : undefined),
95+
300,
96+
[maxActivitiesInput],
97+
);
98+
99+
const queryFilters = useMemo(
100+
() => ({
101+
createdAfter,
102+
createdBefore,
103+
activeAfter,
104+
activeBefore,
105+
minActivities,
106+
maxActivities,
107+
}),
108+
[createdAfter, createdBefore, activeAfter, activeBefore, minActivities, maxActivities],
109+
);
52110

53111
const { users, isLoading, loadMoreUsers, mayLoadMoreUsers, updateUser } = useSpamUsers({
54112
limit: 50,
@@ -57,10 +115,11 @@ const UserSpam = (props: Props) => {
57115
filter,
58116
ordering,
59117
communitySubdomain: communitySubdomain || undefined,
118+
queryFilters,
60119
});
61120

62121
useInfiniteScroll({
63-
scrollTolerance: 100,
122+
scrollTolerance: 500,
64123
useDocumentElement: true,
65124
onRequestMoreItems: loadMoreUsers,
66125
enabled: mayLoadMoreUsers,
@@ -78,6 +137,15 @@ const UserSpam = (props: Props) => {
78137
[updateUser],
79138
);
80139

140+
const handleStatusChanged = useCallback(
141+
(userId: string, status: string) => {
142+
updateUser(userId, {
143+
spamTag: { status } as SpamUser['spamTag'],
144+
} as Partial<SpamUser>);
145+
},
146+
[updateUser],
147+
);
148+
81149
const handleFilterChange = useCallback((newFilter: SpamUsersFilter) => {
82150
setFilter(newFilter);
83151
setOrdering(newFilter.query!.ordering);
@@ -127,13 +195,142 @@ const UserSpam = (props: Props) => {
127195
onChange={(e) => setCommunityInput(e.target.value)}
128196
/>
129197
</label>
198+
<span className="activity-count-filter">
199+
Activities
200+
<input
201+
type="number"
202+
min="0"
203+
placeholder="min"
204+
value={minActivitiesInput}
205+
onChange={(e) => setMinActivitiesInput(e.target.value)}
206+
/>
207+
<span>to</span>
208+
<input
209+
type="number"
210+
min="0"
211+
placeholder="max"
212+
value={maxActivitiesInput}
213+
onChange={(e) => setMaxActivitiesInput(e.target.value)}
214+
/>
215+
</span>
216+
</div>
217+
<div className="date-filters-row">
218+
<div className="date-filter-group">
219+
<span className="date-filter-label">Created</span>
220+
<ButtonGroup minimal>
221+
{datePresets.map((p) => (
222+
<Button
223+
key={p.label}
224+
small
225+
active={createdPreset === p.days}
226+
onClick={() => {
227+
if (createdPreset === p.days) {
228+
setCreatedPreset(null);
229+
setCreatedAfter(undefined);
230+
setCreatedBefore(undefined);
231+
return;
232+
}
233+
setCreatedPreset(p.days);
234+
setCreatedAfter(daysAgo(p.days));
235+
setCreatedBefore(undefined);
236+
}}
237+
>
238+
{p.label}
239+
</Button>
240+
))}
241+
</ButtonGroup>
242+
<input
243+
type="date"
244+
value={createdAfter ? toDateInputValue(createdAfter) : ''}
245+
onChange={(e) => {
246+
setCreatedPreset(null);
247+
setCreatedAfter(fromDateInputValue(e.target.value));
248+
}}
249+
/>
250+
<span>to</span>
251+
<input
252+
type="date"
253+
value={createdBefore ? toDateInputValue(createdBefore) : ''}
254+
onChange={(e) => {
255+
setCreatedPreset(null);
256+
setCreatedBefore(fromDateInputValue(e.target.value));
257+
}}
258+
/>
259+
{(createdAfter || createdBefore) && (
260+
<Button
261+
small
262+
minimal
263+
icon="cross"
264+
onClick={() => {
265+
setCreatedPreset(null);
266+
setCreatedAfter(undefined);
267+
setCreatedBefore(undefined);
268+
}}
269+
/>
270+
)}
271+
</div>
272+
<div className="date-filter-group">
273+
<span className="date-filter-label">Active</span>
274+
<ButtonGroup minimal>
275+
{datePresets.map((p) => (
276+
<Button
277+
key={p.label}
278+
small
279+
active={activePreset === p.days}
280+
onClick={() => {
281+
if (activePreset === p.days) {
282+
setActivePreset(null);
283+
setActiveAfter(undefined);
284+
setActiveBefore(undefined);
285+
return;
286+
}
287+
setActivePreset(p.days);
288+
setActiveAfter(daysAgo(p.days));
289+
setActiveBefore(undefined);
290+
}}
291+
>
292+
{p.label}
293+
</Button>
294+
))}
295+
</ButtonGroup>
296+
<input
297+
type="date"
298+
value={activeAfter ? toDateInputValue(activeAfter) : ''}
299+
onChange={(e) => {
300+
setActivePreset(null);
301+
setActiveAfter(fromDateInputValue(e.target.value));
302+
}}
303+
/>
304+
<span>to</span>
305+
<input
306+
type="date"
307+
value={activeBefore ? toDateInputValue(activeBefore) : ''}
308+
onChange={(e) => {
309+
setActivePreset(null);
310+
setActiveBefore(fromDateInputValue(e.target.value));
311+
}}
312+
/>
313+
{(activeAfter || activeBefore) && (
314+
<Button
315+
small
316+
minimal
317+
icon="cross"
318+
onClick={() => {
319+
setActivePreset(null);
320+
setActiveAfter(undefined);
321+
setActiveBefore(undefined);
322+
}}
323+
/>
324+
)}
325+
</div>
130326
</div>
131327
<div className="users">
132328
{users.map((user) => (
133329
<UserSpamEntry
134330
user={user}
135331
key={user.id}
136332
onSpamTagRemoved={handleSpamTagRemoved}
333+
onStatusChanged={handleStatusChanged}
137334
/>
138335
))}
139336
{!isLoading && users.length === 0 && (

0 commit comments

Comments
 (0)