-import { None, Option, Some } from "@sniptt/monads";
+import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
+import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
+import { RouteComponentProps } from "inferno-router/dist/Route";
import {
AdminPurgeCommentView,
AdminPurgeCommunityView,
AdminPurgePersonView,
AdminPurgePostView,
- CommunityModeratorView,
GetCommunity,
GetCommunityResponse,
GetModlog,
GetModlogResponse,
- GetSiteResponse,
+ GetPersonDetails,
+ GetPersonDetailsResponse,
ModAddCommunityView,
ModAddView,
ModBanFromCommunityView,
ModBanView,
+ ModFeaturePostView,
ModLockPostView,
- ModlogActionType,
ModRemoveCommentView,
ModRemoveCommunityView,
ModRemovePostView,
- ModStickyPostView,
ModTransferCommunityView,
- PersonSafe,
- toUndefined,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ ModlogActionType,
+ Person,
} from "lemmy-js-client";
import moment from "moment";
-import { Subscription } from "rxjs";
import { i18n } from "../i18next";
import { InitialFetchRequest } from "../interfaces";
-import { WebSocketService } from "../services";
+import { FirstLoadService } from "../services/FirstLoadService";
+import { HttpService, RequestState } from "../services/HttpService";
import {
+ Choice,
+ QueryParams,
+ RouteDataResponse,
amAdmin,
amMod,
- auth,
- choicesConfig,
- choicesModLogConfig,
debounce,
fetchLimit,
fetchUsers,
- isBrowser,
+ getIdFromString,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
+ getUpdatedSearchId,
+ myAuth,
+ personToChoice,
setIsoData,
- toast,
- wsClient,
- wsSubscribe,
} from "../utils";
import { HtmlTags } from "./common/html-tags";
-import { Spinner } from "./common/icon";
+import { Icon, Spinner } from "./common/icon";
import { MomentTime } from "./common/moment-time";
import { Paginator } from "./common/paginator";
+import { SearchableSelect } from "./common/searchable-select";
import { CommunityLink } from "./community/community-link";
import { PersonListing } from "./person/person-listing";
-type ModlogType = {
- id: number;
- type_: ModlogActionType;
- moderator: Option<PersonSafe>;
- view:
+
+type FilterType = "mod" | "user";
+
+type View =
| ModRemovePostView
| ModLockPostView
- | ModStickyPostView
+ | ModFeaturePostView
| ModRemoveCommentView
| ModRemoveCommunityView
| ModBanFromCommunityView
| AdminPurgeCommunityView
| AdminPurgePostView
| AdminPurgeCommentView;
+
+type ModlogData = RouteDataResponse<{
+ res: GetModlogResponse;
+ communityRes: GetCommunityResponse;
+ modUserResponse: GetPersonDetailsResponse;
+ userResponse: GetPersonDetailsResponse;
+}>;
+
+interface ModlogType {
+ id: number;
+ type_: ModlogActionType;
+ moderator?: Person;
+ view: View;
when_: string;
-};
-var Choices: any;
-if (isBrowser()) {
- Choices = require("choices.js");
}
+const getModlogQueryParams = () =>
+ getQueryParams<ModlogProps>({
+ actionType: getActionFromString,
+ modId: getIdFromString,
+ userId: getIdFromString,
+ page: getPageFromString,
+ });
+
interface ModlogState {
- res: Option<GetModlogResponse>;
- communityId: Option<number>;
- communityMods: Option<CommunityModeratorView[]>;
- communityName: Option<string>,
+ res: RequestState<GetModlogResponse>;
+ communityRes: RequestState<GetCommunityResponse>;
+ loadingModSearch: boolean;
+ loadingUserSearch: boolean;
+ modSearchOptions: Choice[];
+ userSearchOptions: Choice[];
+}
+
+interface ModlogProps {
page: number;
- siteRes: GetSiteResponse;
- loading: boolean;
- filter_action: ModlogActionType,
- filter_user: Option<number>,
- filter_mod: Option<number>,
+ userId?: number | null;
+ modId?: number | null;
+ actionType: ModlogActionType;
}
-export class Modlog extends Component<any, ModlogState> {
- private isoData = setIsoData(
- this.context,
- GetModlogResponse,
- GetCommunityResponse
- );
- private subscription: Subscription;
- private userChoices: any;
- private modChoices: any;
- private emptyState: ModlogState = {
- res: None,
- communityId: None,
- communityMods: None,
- communityName: None,
- page: 1,
- loading: true,
- siteRes: this.isoData.site_res,
- filter_action: ModlogActionType.All,
- filter_user: None,
- filter_mod: None,
- };
- constructor(props: any, context: any) {
- super(props, context);
- this.state = this.emptyState;
- this.handlePageChange = this.handlePageChange.bind(this);
+function getActionFromString(action?: string): ModlogActionType {
+ return action !== undefined ? (action as ModlogActionType) : "All";
+}
- this.state.communityId = this.props.match.params.community_id
- ? Some(Number(this.props.match.params.community_id))
- : None;
+const getModlogActionMapper =
+ (
+ actionType: ModlogActionType,
+ getAction: (view: View) => { id: number; when_: string }
+ ) =>
+ (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
+ const { id, when_ } = getAction(view);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ return {
+ id,
+ type_: actionType,
+ view,
+ when_,
+ moderator: view.moderator ?? view.admin,
+ };
+ };
- // Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- this.state.res = Some(this.isoData.routeData[0] as GetModlogResponse);
+function buildCombined({
+ removed_comments,
+ locked_posts,
+ featured_posts,
+ removed_communities,
+ removed_posts,
+ added,
+ added_to_community,
+ admin_purged_comments,
+ admin_purged_communities,
+ admin_purged_persons,
+ admin_purged_posts,
+ banned,
+ banned_from_community,
+ transferred_to_community,
+}: GetModlogResponse): ModlogType[] {
+ const combined = removed_posts
+ .map(
+ getModlogActionMapper(
+ "ModRemovePost",
+ ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
+ )
+ )
+ .concat(
+ locked_posts.map(
+ getModlogActionMapper(
+ "ModLockPost",
+ ({ mod_lock_post }: ModLockPostView) => mod_lock_post
+ )
+ )
+ )
+ .concat(
+ featured_posts.map(
+ getModlogActionMapper(
+ "ModFeaturePost",
+ ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
+ )
+ )
+ )
+ .concat(
+ removed_comments.map(
+ getModlogActionMapper(
+ "ModRemoveComment",
+ ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
+ )
+ )
+ )
+ .concat(
+ removed_communities.map(
+ getModlogActionMapper(
+ "ModRemoveCommunity",
+ ({ mod_remove_community }: ModRemoveCommunityView) =>
+ mod_remove_community
+ )
+ )
+ )
+ .concat(
+ banned_from_community.map(
+ getModlogActionMapper(
+ "ModBanFromCommunity",
+ ({ mod_ban_from_community }: ModBanFromCommunityView) =>
+ mod_ban_from_community
+ )
+ )
+ )
+ .concat(
+ added_to_community.map(
+ getModlogActionMapper(
+ "ModAddCommunity",
+ ({ mod_add_community }: ModAddCommunityView) => mod_add_community
+ )
+ )
+ )
+ .concat(
+ transferred_to_community.map(
+ getModlogActionMapper(
+ "ModTransferCommunity",
+ ({ mod_transfer_community }: ModTransferCommunityView) =>
+ mod_transfer_community
+ )
+ )
+ )
+ .concat(
+ added.map(
+ getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add)
+ )
+ )
+ .concat(
+ banned.map(
+ getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban)
+ )
+ )
+ .concat(
+ admin_purged_persons.map(
+ getModlogActionMapper(
+ "AdminPurgePerson",
+ ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
+ )
+ )
+ )
+ .concat(
+ admin_purged_communities.map(
+ getModlogActionMapper(
+ "AdminPurgeCommunity",
+ ({ admin_purge_community }: AdminPurgeCommunityView) =>
+ admin_purge_community
+ )
+ )
+ )
+ .concat(
+ admin_purged_posts.map(
+ getModlogActionMapper(
+ "AdminPurgePost",
+ ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
+ )
+ )
+ )
+ .concat(
+ admin_purged_comments.map(
+ getModlogActionMapper(
+ "AdminPurgeComment",
+ ({ admin_purge_comment }: AdminPurgeCommentView) =>
+ admin_purge_comment
+ )
+ )
+ );
- if (this.isoData.routeData[1]) {
- // Getting the moderators
- let communityRes = Some(
- this.isoData.routeData[1] as GetCommunityResponse
- );
- this.state.communityMods = communityRes.map(c => c.moderators);
- }
+ // Sort them by time
+ combined.sort((a, b) => b.when_.localeCompare(a.when_));
- this.state.loading = false;
- } else {
- this.refetch();
- }
- }
+ return combined;
+}
- componentDidMount() {
- this.setupUserFilter();
- this.setupModFilter();
- }
+function renderModlogType({ type_, view }: ModlogType) {
+ switch (type_) {
+ case "ModRemovePost": {
+ const mrpv = view as ModRemovePostView;
+ const {
+ mod_remove_post: { reason, removed },
+ post: { name, id },
+ } = mrpv;
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription.unsubscribe();
+ return (
+ <>
+ <span>{removed ? "Removed " : "Restored "}</span>
+ <span>
+ Post <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
}
- }
- buildCombined(res: GetModlogResponse): ModlogType[] {
- let removed_posts: ModlogType[] = res.removed_posts.map(r => ({
- id: r.mod_remove_post.id,
- type_: ModlogActionType.ModRemovePost,
- view: r,
- moderator: r.moderator,
- when_: r.mod_remove_post.when_,
- }));
-
- let locked_posts: ModlogType[] = res.locked_posts.map(r => ({
- id: r.mod_lock_post.id,
- type_: ModlogActionType.ModLockPost,
- view: r,
- moderator: r.moderator,
- when_: r.mod_lock_post.when_,
- }));
-
- let stickied_posts: ModlogType[] = res.stickied_posts.map(r => ({
- id: r.mod_sticky_post.id,
- type_: ModlogActionType.ModStickyPost,
- view: r,
- moderator: r.moderator,
- when_: r.mod_sticky_post.when_,
- }));
-
- let removed_comments: ModlogType[] = res.removed_comments.map(r => ({
- id: r.mod_remove_comment.id,
- type_: ModlogActionType.ModRemoveComment,
- view: r,
- moderator: r.moderator,
- when_: r.mod_remove_comment.when_,
- }));
-
- let removed_communities: ModlogType[] = res.removed_communities.map(r => ({
- id: r.mod_remove_community.id,
- type_: ModlogActionType.ModRemoveCommunity,
- view: r,
- moderator: r.moderator,
- when_: r.mod_remove_community.when_,
- }));
-
- let banned_from_community: ModlogType[] = res.banned_from_community.map(
- r => ({
- id: r.mod_ban_from_community.id,
- type_: ModlogActionType.ModBanFromCommunity,
- view: r,
- moderator: r.moderator,
- when_: r.mod_ban_from_community.when_,
- })
- );
+ case "ModLockPost": {
+ const {
+ mod_lock_post: { locked },
+ post: { id, name },
+ } = view as ModLockPostView;
- let added_to_community: ModlogType[] = res.added_to_community.map(r => ({
- id: r.mod_add_community.id,
- type_: ModlogActionType.ModAddCommunity,
- view: r,
- moderator: r.moderator,
- when_: r.mod_add_community.when_,
- }));
-
- let transferred_to_community: ModlogType[] =
- res.transferred_to_community.map(r => ({
- id: r.mod_transfer_community.id,
- type_: ModlogActionType.ModTransferCommunity,
- view: r,
- moderator: r.moderator,
- when_: r.mod_transfer_community.when_,
- }));
-
- let added: ModlogType[] = res.added.map(r => ({
- id: r.mod_add.id,
- type_: ModlogActionType.ModAdd,
- view: r,
- moderator: r.moderator,
- when_: r.mod_add.when_,
- }));
-
- let banned: ModlogType[] = res.banned.map(r => ({
- id: r.mod_ban.id,
- type_: ModlogActionType.ModBan,
- view: r,
- moderator: r.moderator,
- when_: r.mod_ban.when_,
- }));
-
- let purged_persons: ModlogType[] = res.admin_purged_persons.map(r => ({
- id: r.admin_purge_person.id,
- type_: ModlogActionType.AdminPurgePerson,
- view: r,
- moderator: r.admin,
- when_: r.admin_purge_person.when_,
- }));
-
- let purged_communities: ModlogType[] = res.admin_purged_communities.map(
- r => ({
- id: r.admin_purge_community.id,
- type_: ModlogActionType.AdminPurgeCommunity,
- view: r,
- moderator: r.admin,
- when_: r.admin_purge_community.when_,
- })
- );
+ return (
+ <>
+ <span>{locked ? "Locked " : "Unlocked "}</span>
+ <span>
+ Post <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ </>
+ );
+ }
- let purged_posts: ModlogType[] = res.admin_purged_posts.map(r => ({
- id: r.admin_purge_post.id,
- type_: ModlogActionType.AdminPurgePost,
- view: r,
- moderator: r.admin,
- when_: r.admin_purge_post.when_,
- }));
-
- let purged_comments: ModlogType[] = res.admin_purged_comments.map(r => ({
- id: r.admin_purge_comment.id,
- type_: ModlogActionType.AdminPurgeComment,
- view: r,
- moderator: r.admin,
- when_: r.admin_purge_comment.when_,
- }));
-
- let combined: ModlogType[] = [];
-
- combined.push(...removed_posts);
- combined.push(...locked_posts);
- combined.push(...stickied_posts);
- combined.push(...removed_comments);
- combined.push(...removed_communities);
- combined.push(...banned_from_community);
- combined.push(...added_to_community);
- combined.push(...transferred_to_community);
- combined.push(...added);
- combined.push(...banned);
- combined.push(...purged_persons);
- combined.push(...purged_communities);
- combined.push(...purged_posts);
- combined.push(...purged_comments);
-
- // Sort them by time
- combined.sort((a, b) => b.when_.localeCompare(a.when_));
-
- return combined;
- }
+ case "ModFeaturePost": {
+ const {
+ mod_feature_post: { featured, is_featured_community },
+ post: { id, name },
+ } = view as ModFeaturePostView;
- renderModlogType(i: ModlogType) {
- switch (i.type_) {
- case ModlogActionType.ModRemovePost: {
- let mrpv = i.view as ModRemovePostView;
- return [
- mrpv.mod_remove_post.removed.unwrapOr(false)
- ? "Removed "
- : "Restored ",
+ return (
+ <>
+ <span>{featured ? "Featured " : "Unfeatured "}</span>
<span>
- Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
- </span>,
- mrpv.mod_remove_post.reason.match({
- some: reason => <div>reason: {reason}</div>,
- none: <></>,
- }),
- ];
- }
- case ModlogActionType.ModLockPost: {
- let mlpv = i.view as ModLockPostView;
- return [
- mlpv.mod_lock_post.locked.unwrapOr(false) ? "Locked " : "Unlocked ",
- <span>
- Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
- </span>,
- ];
- }
- case ModlogActionType.ModStickyPost: {
- let mspv = i.view as ModStickyPostView;
- return [
- mspv.mod_sticky_post.stickied.unwrapOr(false)
- ? "Stickied "
- : "Unstickied ",
- <span>
- Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link>
- </span>,
- ];
- }
- case ModlogActionType.ModRemoveComment: {
- let mrc = i.view as ModRemoveCommentView;
- return [
- mrc.mod_remove_comment.removed.unwrapOr(false)
- ? "Removed "
- : "Restored ",
+ Post <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ <span>{is_featured_community ? " In Community" : " In Local"}</span>
+ </>
+ );
+ }
+ case "ModRemoveComment": {
+ const mrc = view as ModRemoveCommentView;
+ const {
+ mod_remove_comment: { reason, removed },
+ comment: { id, content },
+ commenter,
+ } = mrc;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Restored "}</span>
<span>
- Comment{" "}
- <Link to={`/post/${mrc.post.id}/comment/${mrc.comment.id}`}>
- {mrc.comment.content}
- </Link>
- </span>,
+ Comment <Link to={`/comment/${id}`}>{content}</Link>
+ </span>
<span>
{" "}
- by <PersonListing person={mrc.commenter} />
- </span>,
- mrc.mod_remove_comment.reason.match({
- some: reason => <div>reason: {reason}</div>,
- none: <></>,
- }),
- ];
- }
- case ModlogActionType.ModRemoveCommunity: {
- let mrco = i.view as ModRemoveCommunityView;
- return [
- mrco.mod_remove_community.removed.unwrapOr(false)
- ? "Removed "
- : "Restored ",
+ by <PersonListing person={commenter} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModRemoveCommunity": {
+ const mrco = view as ModRemoveCommunityView;
+ const {
+ mod_remove_community: { reason, expires, removed },
+ community,
+ } = mrco;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Restored "}</span>
<span>
- Community <CommunityLink community={mrco.community} />
- </span>,
- mrco.mod_remove_community.reason.match({
- some: reason => <div>reason: {reason}</div>,
- none: <></>,
- }),
- mrco.mod_remove_community.expires.match({
- some: expires => (
+ Community <CommunityLink community={community} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ {expires && (
+ <span>
<div>expires: {moment.utc(expires).fromNow()}</div>
- ),
- none: <></>,
- }),
- ];
- }
- case ModlogActionType.ModBanFromCommunity: {
- let mbfc = i.view as ModBanFromCommunityView;
- return [
- <span>
- {mbfc.mod_ban_from_community.banned.unwrapOr(false)
- ? "Banned "
- : "Unbanned "}{" "}
- </span>,
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModBanFromCommunity": {
+ const mbfc = view as ModBanFromCommunityView;
+ const {
+ mod_ban_from_community: { reason, expires, banned },
+ banned_person,
+ community,
+ } = mbfc;
+
+ return (
+ <>
+ <span>{banned ? "Banned " : "Unbanned "}</span>
<span>
- <PersonListing person={mbfc.banned_person} />
- </span>,
- <span> from the community </span>,
+ <PersonListing person={banned_person} />
+ </span>
+ <span> from the community </span>
<span>
- <CommunityLink community={mbfc.community} />
- </span>,
- mbfc.mod_ban_from_community.reason.match({
- some: reason => <div>reason: {reason}</div>,
- none: <></>,
- }),
- mbfc.mod_ban_from_community.expires.match({
- some: expires => (
+ <CommunityLink community={community} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ {expires && (
+ <span>
<div>expires: {moment.utc(expires).fromNow()}</div>
- ),
- none: <></>,
- }),
- ];
- }
- case ModlogActionType.ModAddCommunity: {
- let mac = i.view as ModAddCommunityView;
- return [
- <span>
- {mac.mod_add_community.removed.unwrapOr(false)
- ? "Removed "
- : "Appointed "}{" "}
- </span>,
- <span>
- <PersonListing person={mac.modded_person} />
- </span>,
- <span> as a mod to the community </span>,
- <span>
- <CommunityLink community={mac.community} />
- </span>,
- ];
- }
- case ModlogActionType.ModTransferCommunity: {
- let mtc = i.view as ModTransferCommunityView;
- return [
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModAddCommunity": {
+ const {
+ mod_add_community: { removed },
+ modded_person,
+ community,
+ } = view as ModAddCommunityView;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Appointed "}</span>
<span>
- {mtc.mod_transfer_community.removed.unwrapOr(false)
- ? "Removed "
- : "Transferred "}{" "}
- </span>,
+ <PersonListing person={modded_person} />
+ </span>
+ <span> as a mod to the community </span>
<span>
- <CommunityLink community={mtc.community} />
- </span>,
- <span> to </span>,
+ <CommunityLink community={community} />
+ </span>
+ </>
+ );
+ }
+
+ case "ModTransferCommunity": {
+ const { community, modded_person } = view as ModTransferCommunityView;
+
+ return (
+ <>
+ <span>Transferred</span>
<span>
- <PersonListing person={mtc.modded_person} />
- </span>,
- ];
- }
- case ModlogActionType.ModBan: {
- let mb = i.view as ModBanView;
- return [
+ <CommunityLink community={community} />
+ </span>
+ <span> to </span>
<span>
- {mb.mod_ban.banned.unwrapOr(false) ? "Banned " : "Unbanned "}{" "}
- </span>,
+ <PersonListing person={modded_person} />
+ </span>
+ </>
+ );
+ }
+
+ case "ModBan": {
+ const {
+ mod_ban: { reason, expires, banned },
+ banned_person,
+ } = view as ModBanView;
+
+ return (
+ <>
+ <span>{banned ? "Banned " : "Unbanned "}</span>
<span>
- <PersonListing person={mb.banned_person} />
- </span>,
- mb.mod_ban.reason.match({
- some: reason => <div>reason: {reason}</div>,
- none: <></>,
- }),
- mb.mod_ban.expires.match({
- some: expires => (
+ <PersonListing person={banned_person} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ {expires && (
+ <span>
<div>expires: {moment.utc(expires).fromNow()}</div>
- ),
- none: <></>,
- }),
- ];
- }
- case ModlogActionType.ModAdd: {
- let ma = i.view as ModAddView;
- return [
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModAdd": {
+ const {
+ mod_add: { removed },
+ modded_person,
+ } = view as ModAddView;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Appointed "}</span>
<span>
- {ma.mod_add.removed.unwrapOr(false) ? "Removed " : "Appointed "}{" "}
- </span>,
+ <PersonListing person={modded_person} />
+ </span>
+ <span> as an admin </span>
+ </>
+ );
+ }
+ case "AdminPurgePerson": {
+ const {
+ admin_purge_person: { reason },
+ } = view as AdminPurgePersonView;
+
+ return (
+ <>
+ <span>Purged a Person</span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "AdminPurgeCommunity": {
+ const {
+ admin_purge_community: { reason },
+ } = view as AdminPurgeCommunityView;
+
+ return (
+ <>
+ <span>Purged a Community</span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "AdminPurgePost": {
+ const {
+ admin_purge_post: { reason },
+ community,
+ } = view as AdminPurgePostView;
+
+ return (
+ <>
+ <span>Purged a Post from from </span>
+ <CommunityLink community={community} />
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "AdminPurgeComment": {
+ const {
+ admin_purge_comment: { reason },
+ post: { id, name },
+ } = view as AdminPurgeCommentView;
+
+ return (
+ <>
<span>
- <PersonListing person={ma.modded_person} />
- </span>,
- <span> as an admin </span>,
- ];
- }
- case ModlogActionType.AdminPurgePerson: {
- let ap = i.view as AdminPurgePersonView;
- return [
- <span>Purged a Person</span>,
- ap.admin_purge_person.reason.match({
- some: reason => <div>reason: {reason}</div>,
- none: <></>,
- }),
- ];
- }
- case ModlogActionType.AdminPurgeCommunity: {
- let ap = i.view as AdminPurgeCommunityView;
- return [
- <span>Purged a Community</span>,
- ap.admin_purge_community.reason.match({
- some: reason => <div>reason: {reason}</div>,
- none: <></>,
- }),
- ];
- }
- case ModlogActionType.AdminPurgePost: {
- let ap = i.view as AdminPurgePostView;
- return [
- <span>Purged a Post from from </span>,
- <CommunityLink community={ap.community} />,
- ap.admin_purge_post.reason.match({
- some: reason => <div>reason: {reason}</div>,
- none: <></>,
- }),
- ];
+ Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ default:
+ return <></>;
+ }
+}
+
+const Filter = ({
+ filterType,
+ onChange,
+ value,
+ onSearch,
+ options,
+ loading,
+}: {
+ filterType: FilterType;
+ onChange: (option: Choice) => void;
+ value?: number | null;
+ onSearch: (text: string) => void;
+ options: Choice[];
+ loading: boolean;
+}) => (
+ <div className="col-sm-6 mb-3">
+ <label className="mb-2" htmlFor={`filter-${filterType}`}>
+ {i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
+ </label>
+ <SearchableSelect
+ id={`filter-${filterType}`}
+ value={value ?? 0}
+ options={[
+ {
+ label: i18n.t("all"),
+ value: "0",
+ },
+ ].concat(options)}
+ onChange={onChange}
+ onSearch={onSearch}
+ loading={loading}
+ />
+ </div>
+);
+
+async function createNewOptions({
+ id,
+ oldOptions,
+ text,
+}: {
+ id?: number | null;
+ oldOptions: Choice[];
+ text: string;
+}) {
+ const newOptions: Choice[] = [];
+
+ if (id) {
+ const selectedUser = oldOptions.find(
+ ({ value }) => value === id.toString()
+ );
+
+ if (selectedUser) {
+ newOptions.push(selectedUser);
+ }
+ }
+
+ if (text.length > 0) {
+ newOptions.push(
+ ...(await fetchUsers(text))
+ .slice(0, Number(fetchLimit))
+ .map<Choice>(personToChoice)
+ );
+ }
+
+ return newOptions;
+}
+
+export class Modlog extends Component<
+ RouteComponentProps<{ communityId?: string }>,
+ ModlogState
+> {
+ private isoData = setIsoData<ModlogData>(this.context);
+
+ state: ModlogState = {
+ res: { state: "empty" },
+ communityRes: { state: "empty" },
+ loadingModSearch: false,
+ loadingUserSearch: false,
+ userSearchOptions: [],
+ modSearchOptions: [],
+ };
+
+ constructor(
+ props: RouteComponentProps<{ communityId?: string }>,
+ context: any
+ ) {
+ super(props, context);
+ this.handlePageChange = this.handlePageChange.bind(this);
+ this.handleUserChange = this.handleUserChange.bind(this);
+ this.handleModChange = this.handleModChange.bind(this);
+
+ // Only fetch the data if coming from another route
+ if (FirstLoadService.isFirstLoad) {
+ const { res, communityRes, modUserResponse, userResponse } =
+ this.isoData.routeData;
+
+ this.state = {
+ ...this.state,
+ res,
+ communityRes,
+ };
+
+ if (modUserResponse.state === "success") {
+ this.state = {
+ ...this.state,
+ modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
+ };
}
- case ModlogActionType.AdminPurgeComment: {
- let ap = i.view as AdminPurgeCommentView;
- return [
- <span>
- Purged a Comment from{" "}
- <Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link>
- </span>,
- ap.admin_purge_comment.reason.match({
- some: reason => <div>reason: {reason}</div>,
- none: <></>,
- }),
- ];
+
+ if (userResponse.state === "success") {
+ this.state = {
+ ...this.state,
+ userSearchOptions: [personToChoice(userResponse.data.person_view)],
+ };
}
- default:
- return <div />;
}
}
- combined() {
- let combined = this.state.res.map(this.buildCombined).unwrapOr([]);
+ get combined() {
+ const res = this.state.res;
+ const combined = res.state == "success" ? buildCombined(res.data) : [];
return (
<tbody>
{combined.map(i => (
- <tr>
+ <tr key={i.id}>
<td>
- <MomentTime published={i.when_} updated={None} />
+ <MomentTime published={i.when_} />
</td>
<td>
- {this.amAdminOrMod ? (
- <PersonListing person={i.moderator.unwrap()} />
+ {this.amAdminOrMod && i.moderator ? (
+ <PersonListing person={i.moderator} />
) : (
<div>{this.modOrAdminText(i.moderator)}</div>
)}
</td>
- <td>{this.renderModlogType(i)}</td>
+ <td>{renderModlogType(i)}</td>
</tr>
))}
</tbody>
}
get amAdminOrMod(): boolean {
- return (
- amAdmin(Some(this.state.siteRes.admins)) ||
- amMod(this.state.communityMods)
- );
+ const amMod_ =
+ this.state.communityRes.state == "success" &&
+ amMod(this.state.communityRes.data.moderators);
+ return amAdmin() || amMod_;
}
- modOrAdminText(person: Option<PersonSafe>): string {
- return person.match({
- some: res => this.isoData.site_res.admins.map(a => a.person.id).includes(res.id) ? i18n.t("admin") : i18n.t("mod"),
- none: i18n.t("mod"),
- });
+ modOrAdminText(person?: Person): string {
+ return person &&
+ this.isoData.site_res.admins.some(
+ ({ person: { id } }) => id === person.id
+ )
+ ? i18n.t("admin")
+ : i18n.t("mod");
}
get documentTitle(): string {
- return this.state.siteRes.site_view.match({
- some: siteView => `Modlog - ${siteView.site.name}`,
- none: "",
- });
+ return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
}
render() {
+ const {
+ loadingModSearch,
+ loadingUserSearch,
+ userSearchOptions,
+ modSearchOptions,
+ } = this.state;
+ const { actionType, modId, userId } = getModlogQueryParams();
+
return (
- <div className="container">
+ <div className="modlog container-lg">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
- description={None}
- image={None}
/>
- {this.state.loading ? (
- <h5>
- <Spinner large />
- </h5>
- ) : (
- <div>
+
+ <div>
+ <div
+ className="alert alert-warning text-sm-start text-xs-center"
+ role="alert"
+ >
+ <Icon
+ icon="alert-triangle"
+ inline
+ classes="me-sm-2 mx-auto d-sm-inline d-block"
+ />
+ <T i18nKey="modlog_content_warning" class="d-inline">
+ #<strong>#</strong>#
+ </T>
+ </div>
+ {this.state.communityRes.state === "success" && (
<h5>
- {this.state.communityName.isSome() && (
- <Link
- className="text-body"
- to={`/c/${this.state.communityName}`}
- >
- /c/{this.state.communityName}{" "}
- </Link>
- )}
+ <Link
+ className="text-body"
+ to={`/c/${this.state.communityRes.data.community_view.community.name}`}
+ >
+ /c/{this.state.communityRes.data.community_view.community.name}{" "}
+ </Link>
<span>{i18n.t("modlog")}</span>
</h5>
- <form className="form-inline mr-2">
+ )}
+ <div className="row mb-2">
+ <div className="col-sm-6">
<select
- value={this.state.filter_action}
+ value={actionType}
onChange={linkEvent(this, this.handleFilterActionChange)}
- className="custom-select col-4 mb-2"
- aria-label="action">
- <option disabled aria-hidden="true">{i18n.t("filter_by_action")}</option>
- <option value={ModlogActionType.All}>{i18n.t("all")}</option>
- <option value={ModlogActionType.ModRemovePost}>Removing Posts</option>
- <option value={ModlogActionType.ModLockPost}>Locking Posts</option>
- <option value={ModlogActionType.ModStickyPost}>Stickying Posts</option>
- <option value={ModlogActionType.ModRemoveComment}>Removing Comments</option>
- <option value={ModlogActionType.ModRemoveCommunity}>Removing Communities</option>
- <option value={ModlogActionType.ModBanFromCommunity}>Banning From Communities</option>
- <option value={ModlogActionType.ModAddCommunity}>Adding Mod to Community</option>
- <option value={ModlogActionType.ModTransferCommunity}>Transfering Communities</option>
- <option value={ModlogActionType.ModAdd}>Adding Mod to Site</option>
- <option value={ModlogActionType.ModBan}>Banning From Site</option>
+ className="form-select"
+ aria-label="action"
+ >
+ <option disabled aria-hidden="true">
+ {i18n.t("filter_by_action")}
+ </option>
+ <option value={"All"}>{i18n.t("all")}</option>
+ <option value={"ModRemovePost"}>Removing Posts</option>
+ <option value={"ModLockPost"}>Locking Posts</option>
+ <option value={"ModFeaturePost"}>Featuring Posts</option>
+ <option value={"ModRemoveComment"}>Removing Comments</option>
+ <option value={"ModRemoveCommunity"}>
+ Removing Communities
+ </option>
+ <option value={"ModBanFromCommunity"}>
+ Banning From Communities
+ </option>
+ <option value={"ModAddCommunity"}>
+ Adding Mod to Community
+ </option>
+ <option value={"ModTransferCommunity"}>
+ Transferring Communities
+ </option>
+ <option value={"ModAdd"}>Adding Mod to Site</option>
+ <option value={"ModBan"}>Banning From Site</option>
</select>
- {
- this.state.siteRes.site_view.match({
- some: site_view => !site_view.site.hide_modlog_mod_names.unwrapOr(false) && (
- <select
- id="filter-mod"
- value={toUndefined(this.state.filter_mod)}>
- <option>{i18n.t("filter_by_mod")}</option>
- </select>
- ),
- none: <></>,
- })
- }
- <select
- id="filter-user"
- value={toUndefined(this.state.filter_user)}>
- <option>{i18n.t("filter_by_user")}</option>
- </select>
- </form>
- <div className="table-responsive">
- <table id="modlog_table" className="table table-sm table-hover">
- <thead className="pointer">
- <tr>
- <th> {i18n.t("time")}</th>
- <th>{i18n.t("mod")}</th>
- <th>{i18n.t("action")}</th>
- </tr>
- </thead>
- {this.combined()}
- </table>
- <Paginator
- page={this.state.page}
- onChange={this.handlePageChange}
- />
</div>
</div>
- )}
+ <div className="row mb-2">
+ <Filter
+ filterType="user"
+ onChange={this.handleUserChange}
+ onSearch={this.handleSearchUsers}
+ value={userId}
+ options={userSearchOptions}
+ loading={loadingUserSearch}
+ />
+ {!this.isoData.site_res.site_view.local_site
+ .hide_modlog_mod_names && (
+ <Filter
+ filterType="mod"
+ onChange={this.handleModChange}
+ onSearch={this.handleSearchMods}
+ value={modId}
+ options={modSearchOptions}
+ loading={loadingModSearch}
+ />
+ )}
+ </div>
+ {this.renderModlogTable()}
+ </div>
</div>
);
}
+ renderModlogTable() {
+ switch (this.state.res.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const page = getModlogQueryParams().page;
+ return (
+ <div className="table-responsive">
+ <table id="modlog_table" className="table table-sm table-hover">
+ <thead className="pointer">
+ <tr>
+ <th> {i18n.t("time")}</th>
+ <th>{i18n.t("mod")}</th>
+ <th>{i18n.t("action")}</th>
+ </tr>
+ </thead>
+ {this.combined}
+ </table>
+ <Paginator page={page} onChange={this.handlePageChange} />
+ </div>
+ );
+ }
+ }
+ }
+
handleFilterActionChange(i: Modlog, event: any) {
- i.setState({ filter_action: event.target.value });
- i.refetch();
+ i.updateUrl({
+ actionType: event.target.value as ModlogActionType,
+ page: 1,
+ });
}
- handlePageChange(val: number) {
- this.setState({ page: val });
- this.refetch();
+ handlePageChange(page: number) {
+ this.updateUrl({ page });
}
- refetch() {
- let modlogForm = new GetModlog({
- community_id: this.state.communityId,
- page: Some(this.state.page),
- limit: Some(fetchLimit),
- auth: auth(false).ok(),
- type_: this.state.filter_action,
- other_person_id: this.state.filter_user,
- mod_person_id: this.state.filter_mod,
+ handleUserChange(option: Choice) {
+ this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
+ }
+
+ handleModChange(option: Choice) {
+ this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
+ }
+
+ handleSearchUsers = debounce(async (text: string) => {
+ const { userId } = getModlogQueryParams();
+ const { userSearchOptions } = this.state;
+ this.setState({ loadingUserSearch: true });
+
+ const newOptions = await createNewOptions({
+ id: userId,
+ text,
+ oldOptions: userSearchOptions,
+ });
+
+ this.setState({
+ userSearchOptions: newOptions,
+ loadingUserSearch: false,
});
- WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
-
- this.state.communityId.match({
- some: id => {
- let communityForm = new GetCommunity({
- id: Some(id),
- name: None,
- auth: auth(false).ok(),
- });
- WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
- },
- none: void 0,
+ });
+
+ handleSearchMods = debounce(async (text: string) => {
+ const { modId } = getModlogQueryParams();
+ const { modSearchOptions } = this.state;
+ this.setState({ loadingModSearch: true });
+
+ const newOptions = await createNewOptions({
+ id: modId,
+ text,
+ oldOptions: modSearchOptions,
});
- }
- setupUserFilter() {
- if (isBrowser()) {
- let selectId: any = document.getElementById("filter-user");
- if (selectId) {
- this.userChoices = new Choices(selectId, choicesModLogConfig);
- this.userChoices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.state.filter_user = Some(Number(e.detail.choice.value));
- this.setState(this.state);
- this.refetch();
- },
- false
- );
- this.userChoices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let users = (await fetchUsers(e.detail.value)).users;
- this.userChoices.setChoices(
- users.map(u => {return {
- value: u.person.id.toString(),
- label: u.person.name,
- }}),
- "value",
- "label",
- true
- );
- } catch (err) {
- console.log(err);
- }
- }),
- false
- );
- }
- }
+ this.setState({
+ modSearchOptions: newOptions,
+ loadingModSearch: false,
+ });
+ });
+
+ async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
+ const {
+ page: urlPage,
+ actionType: urlActionType,
+ modId: urlModId,
+ userId: urlUserId,
+ } = getModlogQueryParams();
+
+ const queryParams: QueryParams<ModlogProps> = {
+ page: (page ?? urlPage).toString(),
+ actionType: actionType ?? urlActionType,
+ modId: getUpdatedSearchId(modId, urlModId),
+ userId: getUpdatedSearchId(userId, urlUserId),
+ };
+
+ const communityId = this.props.match.params.communityId;
+
+ this.props.history.push(
+ `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
+ queryParams
+ )}`
+ );
+
+ await this.refetch();
}
- setupModFilter() {
- if (isBrowser()) {
- let selectId: any = document.getElementById("filter-mod");
- if (selectId) {
- this.modChoices = new Choices(selectId, choicesModLogConfig);
- this.modChoices.passedElement.element.addEventListener(
- "choice",
- (e: any) => {
- this.state.filter_mod = Some(Number(e.detail.choice.value));
- this.setState(this.state);
- this.refetch();
- },
- false
- );
- this.modChoices.passedElement.element.addEventListener(
- "search",
- debounce(async (e: any) => {
- try {
- let mods = (await fetchUsers(e.detail.value)).users;
- this.modChoices.setChoices(
- mods.map(u => {return {
- value: u.person.id.toString(),
- label: u.person.name,
- }}),
- "value",
- "label",
- true
- );
- } catch (err) {
- console.log(err);
- }
- }),
- false
- );
- }
+ async refetch() {
+ const auth = myAuth();
+ const { actionType, page, modId, userId } = getModlogQueryParams();
+ const { communityId: urlCommunityId } = this.props.match.params;
+ const communityId = getIdFromString(urlCommunityId);
+
+ this.setState({ res: { state: "loading" } });
+ this.setState({
+ res: await HttpService.client.getModlog({
+ community_id: communityId,
+ page,
+ limit: fetchLimit,
+ type_: actionType,
+ other_person_id: userId ?? undefined,
+ mod_person_id: !this.isoData.site_res.site_view.local_site
+ .hide_modlog_mod_names
+ ? modId ?? undefined
+ : undefined,
+ auth,
+ }),
+ });
+
+ if (communityId) {
+ this.setState({ communityRes: { state: "loading" } });
+ this.setState({
+ communityRes: await HttpService.client.getCommunity({
+ id: communityId,
+ auth,
+ }),
+ });
}
}
- static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
- let pathSplit = req.path.split("/");
- let communityId = Some(pathSplit[3]).map(Number);
- let promises: Promise<any>[] = [];
+ static async fetchInitialData({
+ client,
+ path,
+ query: { modId: urlModId, page, userId: urlUserId, actionType },
+ auth,
+ site,
+ }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
+ const pathSplit = path.split("/");
+ const communityId = getIdFromString(pathSplit[2]);
+ const modId = !site.site_view.local_site.hide_modlog_mod_names
+ ? getIdFromString(urlModId)
+ : undefined;
+ const userId = getIdFromString(urlUserId);
- let modlogForm = new GetModlog({
- page: Some(1),
- limit: Some(fetchLimit),
+ const modlogForm: GetModlog = {
+ page: getPageFromString(page),
+ limit: fetchLimit,
community_id: communityId,
- mod_person_id: None,
- auth: req.auth,
- type_: ModlogActionType.All,
- other_person_id: None
- });
+ type_: getActionFromString(actionType),
+ mod_person_id: modId,
+ other_person_id: userId,
+ auth,
+ };
- promises.push(req.client.getModlog(modlogForm));
+ let communityResponse: RequestState<GetCommunityResponse> = {
+ state: "empty",
+ };
- if (communityId.isSome()) {
- let communityForm = new GetCommunity({
+ if (communityId) {
+ const communityForm: GetCommunity = {
id: communityId,
- name: None,
- auth: req.auth,
- });
- promises.push(req.client.getCommunity(communityForm));
- } else {
- promises.push(Promise.resolve());
+ auth,
+ };
+
+ communityResponse = await client.getCommunity(communityForm);
}
- return promises;
- }
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- return;
- } else if (op == UserOperation.GetModlog) {
- let data = wsJsonToRes<GetModlogResponse>(msg, GetModlogResponse);
- this.state.loading = false;
- window.scrollTo(0, 0);
- this.state.res = Some(data);
- this.setState(this.state);
- } else if (op == UserOperation.GetCommunity) {
- let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
- this.state.communityMods = Some(data.moderators);
- this.state.communityName = Some(data.community_view.community.name);
+ let modUserResponse: RequestState<GetPersonDetailsResponse> = {
+ state: "empty",
+ };
+
+ if (modId) {
+ const getPersonForm: GetPersonDetails = {
+ person_id: modId,
+ auth,
+ };
+
+ modUserResponse = await client.getPersonDetails(getPersonForm);
}
+
+ let userResponse: RequestState<GetPersonDetailsResponse> = {
+ state: "empty",
+ };
+
+ if (userId) {
+ const getPersonForm: GetPersonDetails = {
+ person_id: userId,
+ auth,
+ };
+
+ userResponse = await client.getPersonDetails(getPersonForm);
+ }
+
+ return {
+ res: await client.getModlog(modlogForm),
+ communityRes: communityResponse,
+ modUserResponse,
+ userResponse,
+ };
}
}