-import { Component } from "inferno";
+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 {
- CommunityModeratorView,
+ AdminPurgeCommentView,
+ AdminPurgeCommunityView,
+ AdminPurgePersonView,
+ AdminPurgePostView,
GetCommunity,
GetCommunityResponse,
GetModlog,
GetModlogResponse,
+ GetPersonDetails,
+ GetPersonDetailsResponse,
ModAddCommunityView,
ModAddView,
ModBanFromCommunityView,
ModBanView,
+ ModFeaturePostView,
ModLockPostView,
ModRemoveCommentView,
ModRemoveCommunityView,
ModRemovePostView,
- ModStickyPostView,
ModTransferCommunityView,
- PersonSafe,
- SiteView,
- UserOperation,
+ ModlogActionType,
+ Person,
} from "lemmy-js-client";
import moment from "moment";
-import { Subscription } from "rxjs";
import { i18n } from "../i18next";
import { InitialFetchRequest } from "../interfaces";
-import { UserService, WebSocketService } from "../services";
+import { FirstLoadService } from "../services/FirstLoadService";
+import { HttpService, RequestState } from "../services/HttpService";
import {
- authField,
+ Choice,
+ QueryParams,
+ RouteDataResponse,
+ amAdmin,
+ amMod,
+ debounce,
fetchLimit,
- isBrowser,
+ fetchUsers,
+ getIdFromString,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
+ getUpdatedSearchId,
+ myAuth,
+ personToChoice,
setIsoData,
- setOptionalAuth,
- toast,
- wsClient,
- wsJsonToRes,
- wsSubscribe,
- wsUserOp,
} 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";
-enum ModlogEnum {
- ModRemovePost,
- ModLockPost,
- ModStickyPost,
- ModRemoveComment,
- ModRemoveCommunity,
- ModBanFromCommunity,
- ModAddCommunity,
- ModTransferCommunity,
- ModAdd,
- ModBan,
-}
+type FilterType = "mod" | "user";
+
+type View =
+ | ModRemovePostView
+ | ModLockPostView
+ | ModFeaturePostView
+ | ModRemoveCommentView
+ | ModRemoveCommunityView
+ | ModBanFromCommunityView
+ | ModBanView
+ | ModAddCommunityView
+ | ModTransferCommunityView
+ | ModAddView
+ | AdminPurgePersonView
+ | AdminPurgeCommunityView
+ | AdminPurgePostView
+ | AdminPurgeCommentView;
+
+type ModlogData = RouteDataResponse<{
+ res: GetModlogResponse;
+ communityRes: GetCommunityResponse;
+ modUserResponse: GetPersonDetailsResponse;
+ userResponse: GetPersonDetailsResponse;
+}>;
-type ModlogType = {
+interface ModlogType {
id: number;
- type_: ModlogEnum;
- view:
- | ModRemovePostView
- | ModLockPostView
- | ModStickyPostView
- | ModRemoveCommentView
- | ModRemoveCommunityView
- | ModBanFromCommunityView
- | ModBanView
- | ModAddCommunityView
- | ModTransferCommunityView
- | ModAddView;
+ type_: ModlogActionType;
+ moderator?: Person;
+ view: View;
when_: string;
-};
+}
+
+const getModlogQueryParams = () =>
+ getQueryParams<ModlogProps>({
+ actionType: getActionFromString,
+ modId: getIdFromString,
+ userId: getIdFromString,
+ page: getPageFromString,
+ });
interface ModlogState {
- res: GetModlogResponse;
- communityId?: number;
- communityName?: string;
- communityMods?: CommunityModeratorView[];
+ res: RequestState<GetModlogResponse>;
+ communityRes: RequestState<GetCommunityResponse>;
+ loadingModSearch: boolean;
+ loadingUserSearch: boolean;
+ modSearchOptions: Choice[];
+ userSearchOptions: Choice[];
+}
+
+interface ModlogProps {
page: number;
- site_view: SiteView;
- loading: boolean;
+ userId?: number | null;
+ modId?: number | null;
+ actionType: ModlogActionType;
}
-export class Modlog extends Component<any, ModlogState> {
- private isoData = setIsoData(this.context);
- private subscription: Subscription;
- private emptyState: ModlogState = {
- res: {
- removed_posts: [],
- locked_posts: [],
- stickied_posts: [],
- removed_comments: [],
- removed_communities: [],
- banned_from_community: [],
- banned: [],
- added_to_community: [],
- transferred_to_community: [],
- added: [],
- },
- page: 1,
- loading: true,
- site_view: this.isoData.site_res.site_view,
- };
+function getActionFromString(action?: string): ModlogActionType {
+ return action !== undefined ? (action as ModlogActionType) : "All";
+}
- constructor(props: any, context: any) {
- super(props, context);
+const getModlogActionMapper =
+ (
+ actionType: ModlogActionType,
+ getAction: (view: View) => { id: number; when_: string }
+ ) =>
+ (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
+ const { id, when_ } = getAction(view);
- this.state = this.emptyState;
- this.handlePageChange = this.handlePageChange.bind(this);
+ return {
+ id,
+ type_: actionType,
+ view,
+ when_,
+ moderator: view.moderator ?? view.admin,
+ };
+ };
- this.state.communityId = this.props.match.params.community_id
- ? Number(this.props.match.params.community_id)
- : undefined;
+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
+ )
+ )
+ );
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ // Sort them by time
+ combined.sort((a, b) => b.when_.localeCompare(a.when_));
- // Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- let data = this.isoData.routeData[0];
- this.state.res = data;
- this.state.loading = false;
-
- // Getting the moderators
- if (this.isoData.routeData[1]) {
- this.state.communityMods = this.isoData.routeData[1].moderators;
- }
- } else {
- this.refetch();
- }
- }
+ return combined;
+}
+
+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_: ModlogEnum.ModRemovePost,
- view: r,
- when_: r.mod_remove_post.when_,
- }));
-
- let locked_posts: ModlogType[] = res.locked_posts.map(r => ({
- id: r.mod_lock_post.id,
- type_: ModlogEnum.ModLockPost,
- view: r,
- when_: r.mod_lock_post.when_,
- }));
-
- let stickied_posts: ModlogType[] = res.stickied_posts.map(r => ({
- id: r.mod_sticky_post.id,
- type_: ModlogEnum.ModStickyPost,
- view: r,
- when_: r.mod_sticky_post.when_,
- }));
-
- let removed_comments: ModlogType[] = res.removed_comments.map(r => ({
- id: r.mod_remove_comment.id,
- type_: ModlogEnum.ModRemoveComment,
- view: r,
- when_: r.mod_remove_comment.when_,
- }));
-
- let removed_communities: ModlogType[] = res.removed_communities.map(r => ({
- id: r.mod_remove_community.id,
- type_: ModlogEnum.ModRemoveCommunity,
- view: r,
- 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_: ModlogEnum.ModBanFromCommunity,
- view: r,
- 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_: ModlogEnum.ModAddCommunity,
- view: r,
- when_: r.mod_add_community.when_,
- }));
-
- let transferred_to_community: ModlogType[] =
- res.transferred_to_community.map(r => ({
- id: r.mod_transfer_community.id,
- type_: ModlogEnum.ModTransferCommunity,
- view: r,
- when_: r.mod_transfer_community.when_,
- }));
-
- let added: ModlogType[] = res.added.map(r => ({
- id: r.mod_add.id,
- type_: ModlogEnum.ModAdd,
- view: r,
- when_: r.mod_add.when_,
- }));
-
- let banned: ModlogType[] = res.banned.map(r => ({
- id: r.mod_ban.id,
- type_: ModlogEnum.ModBan,
- view: r,
- when_: r.mod_ban.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);
-
- if (this.state.communityId && combined.length > 0) {
- this.state.communityName = (
- combined[0].view as ModRemovePostView
- ).community.name;
+ return (
+ <>
+ <span>{locked ? "Locked " : "Unlocked "}</span>
+ <span>
+ Post <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ </>
+ );
}
- // Sort them by time
- combined.sort((a, b) => b.when_.localeCompare(a.when_));
+ case "ModFeaturePost": {
+ const {
+ mod_feature_post: { featured, is_featured_community },
+ post: { id, name },
+ } = view as ModFeaturePostView;
- return combined;
- }
-
- renderModlogType(i: ModlogType) {
- switch (i.type_) {
- case ModlogEnum.ModRemovePost: {
- let mrpv = i.view as ModRemovePostView;
- return [
- mrpv.mod_remove_post.removed ? "Removed " : "Restored ",
- <span>
- Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
- </span>,
- mrpv.mod_remove_post.reason &&
- ` reason: ${mrpv.mod_remove_post.reason}`,
- ];
- }
- case ModlogEnum.ModLockPost: {
- let mlpv = i.view as ModLockPostView;
- return [
- mlpv.mod_lock_post.locked ? "Locked " : "Unlocked ",
- <span>
- Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
- </span>,
- ];
- }
- case ModlogEnum.ModStickyPost: {
- let mspv = i.view as ModStickyPostView;
- return [
- mspv.mod_sticky_post.stickied ? "Stickied " : "Unstickied ",
+ return (
+ <>
+ <span>{featured ? "Featured " : "Unfeatured "}</span>
<span>
- Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link>
- </span>,
- ];
- }
- case ModlogEnum.ModRemoveComment: {
- let mrc = i.view as ModRemoveCommentView;
- return [
- mrc.mod_remove_comment.removed ? "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 &&
- ` reason: ${mrc.mod_remove_comment.reason}`,
- ];
- }
- case ModlogEnum.ModRemoveCommunity: {
- let mrco = i.view as ModRemoveCommunityView;
- return [
- mrco.mod_remove_community.removed ? "Removed " : "Restored ",
- <span>
- Community <CommunityLink community={mrco.community} />
- </span>,
- mrco.mod_remove_community.reason &&
- ` reason: ${mrco.mod_remove_community.reason}`,
- mrco.mod_remove_community.expires &&
- ` expires: ${moment
- .utc(mrco.mod_remove_community.expires)
- .fromNow()}`,
- ];
- }
- case ModlogEnum.ModBanFromCommunity: {
- let mbfc = i.view as ModBanFromCommunityView;
- return [
+ 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>
- {mbfc.mod_ban_from_community.banned ? "Banned " : "Unbanned "}{" "}
- </span>,
+ Community <CommunityLink community={community} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ {expires && (
+ <span>
+ <div>expires: {moment.utc(expires).fromNow()}</div>
+ </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>,
- <div>
- {mbfc.mod_ban_from_community.reason &&
- ` reason: ${mbfc.mod_ban_from_community.reason}`}
- </div>,
- <div>
- {mbfc.mod_ban_from_community.expires &&
- ` expires: ${moment
- .utc(mbfc.mod_ban_from_community.expires)
- .fromNow()}`}
- </div>,
- ];
- }
- case ModlogEnum.ModAddCommunity: {
- let mac = i.view as ModAddCommunityView;
- return [
+ <CommunityLink community={community} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ {expires && (
+ <span>
+ <div>expires: {moment.utc(expires).fromNow()}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModAddCommunity": {
+ const {
+ mod_add_community: { removed },
+ modded_person,
+ community,
+ } = view as ModAddCommunityView;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Appointed "}</span>
<span>
- {mac.mod_add_community.removed ? "Removed " : "Appointed "}{" "}
- </span>,
+ <PersonListing person={modded_person} />
+ </span>
+ <span> as a mod to the community </span>
<span>
- <PersonListing person={mac.modded_person} />
- </span>,
- <span> as a mod to the community </span>,
+ <CommunityLink community={community} />
+ </span>
+ </>
+ );
+ }
+
+ case "ModTransferCommunity": {
+ const { community, modded_person } = view as ModTransferCommunityView;
+
+ return (
+ <>
+ <span>Transferred</span>
<span>
- <CommunityLink community={mac.community} />
- </span>,
- ];
- }
- case ModlogEnum.ModTransferCommunity: {
- let mtc = i.view as ModTransferCommunityView;
- return [
+ <CommunityLink community={community} />
+ </span>
+ <span> to </span>
<span>
- {mtc.mod_transfer_community.removed ? "Removed " : "Transferred "}{" "}
- </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>
- <CommunityLink community={mtc.community} />
- </span>,
- <span> to </span>,
+ <PersonListing person={banned_person} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ {expires && (
+ <span>
+ <div>expires: {moment.utc(expires).fromNow()}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModAdd": {
+ const {
+ mod_add: { removed },
+ modded_person,
+ } = view as ModAddView;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Appointed "}</span>
<span>
- <PersonListing person={mtc.modded_person} />
- </span>,
- ];
- }
- case ModlogEnum.ModBan: {
- let mb = i.view as ModBanView;
- return [
- <span>{mb.mod_ban.banned ? "Banned " : "Unbanned "} </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={mb.banned_person} />
- </span>,
- <div>{mb.mod_ban.reason && ` reason: ${mb.mod_ban.reason}`}</div>,
- <div>
- {mb.mod_ban.expires &&
- ` expires: ${moment.utc(mb.mod_ban.expires).fromNow()}`}
- </div>,
- ];
+ 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 ModlogEnum.ModAdd: {
- let ma = i.view as ModAddView;
- return [
- <span>{ma.mod_add.removed ? "Removed " : "Appointed "} </span>,
- <span>
- <PersonListing person={ma.modded_person} />
- </span>,
- <span> as an admin </span>,
- ];
+
+ if (userResponse.state === "success") {
+ this.state = {
+ ...this.state,
+ userSearchOptions: [personToChoice(userResponse.data.person_view)],
+ };
}
- default:
- return <div />;
}
}
- combined() {
- let combined = this.buildCombined(this.state.res);
+ 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 data={i} />
+ <MomentTime published={i.when_} />
</td>
<td>
- {this.isAdminOrMod ? (
- <PersonListing person={i.view.moderator} />
+ {this.amAdminOrMod && i.moderator ? (
+ <PersonListing person={i.moderator} />
) : (
- <div>{this.modOrAdminText(i.view.moderator)}</div>
+ <div>{this.modOrAdminText(i.moderator)}</div>
)}
</td>
- <td>{this.renderModlogType(i)}</td>
+ <td>{renderModlogType(i)}</td>
</tr>
))}
</tbody>
);
}
- get isAdminOrMod(): boolean {
- let isAdmin =
- UserService.Instance.myUserInfo &&
- this.isoData.site_res.admins
- .map(a => a.person.id)
- .includes(UserService.Instance.myUserInfo.local_user_view.person.id);
- let isMod =
- UserService.Instance.myUserInfo &&
- this.state.communityMods &&
- this.state.communityMods
- .map(m => m.moderator.id)
- .includes(UserService.Instance.myUserInfo.local_user_view.person.id);
- return isAdmin || isMod;
+ get amAdminOrMod(): boolean {
+ const amMod_ =
+ this.state.communityRes.state == "success" &&
+ amMod(this.state.communityRes.data.moderators);
+ return amAdmin() || amMod_;
}
- modOrAdminText(person: PersonSafe): Text {
- if (
- this.isoData.site_res.admins.map(a => a.person.id).includes(person.id)
- ) {
- return i18n.t("admin");
- } else {
- return 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 `Modlog - ${this.state.site_view.site.name}`;
+ 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 class="container">
+ <div className="modlog container-lg">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
- {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 && (
- <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>
- <div class="table-responsive">
- <table id="modlog_table" class="table table-sm table-hover">
- <thead class="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 className="row mb-2">
+ <div className="col-sm-6">
+ <select
+ value={actionType}
+ onChange={linkEvent(this, this.handleFilterActionChange)}
+ 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>
</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>
);
}
- handlePageChange(val: number) {
- this.setState({ page: val });
- this.refetch();
+ 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>
+ );
+ }
+ }
}
- refetch() {
- let modlogForm: GetModlog = {
- community_id: this.state.communityId,
- page: this.state.page,
- limit: fetchLimit,
- auth: authField(false),
+ handleFilterActionChange(i: Modlog, event: any) {
+ i.updateUrl({
+ actionType: event.target.value as ModlogActionType,
+ page: 1,
+ });
+ }
+
+ handlePageChange(page: number) {
+ this.updateUrl({ page });
+ }
+
+ 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,
+ });
+ });
+
+ 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,
+ });
+
+ 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),
};
- WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
- if (this.state.communityId) {
- let communityForm: GetCommunity = {
- id: this.state.communityId,
- name: this.state.communityName,
- };
- WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
+ const communityId = this.props.match.params.communityId;
+
+ this.props.history.push(
+ `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
+ queryParams
+ )}`
+ );
+
+ await this.refetch();
+ }
+
+ 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 = pathSplit[3];
- 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: GetModlog = {
- page: 1,
+ const modlogForm: GetModlog = {
+ page: getPageFromString(page),
limit: fetchLimit,
+ community_id: communityId,
+ type_: getActionFromString(actionType),
+ mod_person_id: modId,
+ other_person_id: userId,
+ auth,
+ };
+
+ let communityResponse: RequestState<GetCommunityResponse> = {
+ state: "empty",
};
if (communityId) {
- modlogForm.community_id = Number(communityId);
+ const communityForm: GetCommunity = {
+ id: communityId,
+ auth,
+ };
+
+ communityResponse = await client.getCommunity(communityForm);
}
- setOptionalAuth(modlogForm, req.auth);
- promises.push(req.client.getModlog(modlogForm));
+ let modUserResponse: RequestState<GetPersonDetailsResponse> = {
+ state: "empty",
+ };
- if (communityId) {
- let communityForm: GetCommunity = {
- id: Number(communityId),
+ if (modId) {
+ const getPersonForm: GetPersonDetails = {
+ person_id: modId,
+ auth,
};
- setOptionalAuth(communityForm, req.auth);
- promises.push(req.client.getCommunity(communityForm));
+
+ modUserResponse = await client.getPersonDetails(getPersonForm);
}
- 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).data;
- this.state.loading = false;
- window.scrollTo(0, 0);
- this.state.res = data;
- this.setState(this.state);
- } else if (op == UserOperation.GetCommunity) {
- let data = wsJsonToRes<GetCommunityResponse>(msg).data;
- this.state.communityMods = data.moderators;
+ 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,
+ };
}
}