1 import { debounce, getQueryParams, getQueryString } from "@utils/helpers";
2 import { amAdmin, amMod } from "@utils/roles";
3 import type { QueryParams } from "@utils/types";
4 import { NoOptionI18nKeys } from "i18next";
5 import { Component, linkEvent } from "inferno";
6 import { T } from "inferno-i18next-dess";
7 import { Link } from "inferno-router";
8 import { RouteComponentProps } from "inferno-router/dist/Route";
10 AdminPurgeCommentView,
11 AdminPurgeCommunityView,
19 GetPersonDetailsResponse,
22 ModBanFromCommunityView,
27 ModRemoveCommunityView,
29 ModTransferCommunityView,
32 } from "lemmy-js-client";
33 import moment from "moment";
34 import { i18n } from "../i18next";
35 import { InitialFetchRequest } from "../interfaces";
36 import { FirstLoadService } from "../services/FirstLoadService";
37 import { HttpService, RequestState } from "../services/HttpService";
50 import { HtmlTags } from "./common/html-tags";
51 import { Icon, Spinner } from "./common/icon";
52 import { MomentTime } from "./common/moment-time";
53 import { Paginator } from "./common/paginator";
54 import { SearchableSelect } from "./common/searchable-select";
55 import { CommunityLink } from "./community/community-link";
56 import { PersonListing } from "./person/person-listing";
58 type FilterType = "mod" | "user";
64 | ModRemoveCommentView
65 | ModRemoveCommunityView
66 | ModBanFromCommunityView
69 | ModTransferCommunityView
71 | AdminPurgePersonView
72 | AdminPurgeCommunityView
74 | AdminPurgeCommentView;
76 type ModlogData = RouteDataResponse<{
77 res: GetModlogResponse;
78 communityRes: GetCommunityResponse;
79 modUserResponse: GetPersonDetailsResponse;
80 userResponse: GetPersonDetailsResponse;
83 interface ModlogType {
85 type_: ModlogActionType;
91 const getModlogQueryParams = () =>
92 getQueryParams<ModlogProps>({
93 actionType: getActionFromString,
94 modId: getIdFromString,
95 userId: getIdFromString,
96 page: getPageFromString,
99 interface ModlogState {
100 res: RequestState<GetModlogResponse>;
101 communityRes: RequestState<GetCommunityResponse>;
102 loadingModSearch: boolean;
103 loadingUserSearch: boolean;
104 modSearchOptions: Choice[];
105 userSearchOptions: Choice[];
108 interface ModlogProps {
110 userId?: number | null;
111 modId?: number | null;
112 actionType: ModlogActionType;
115 function getActionFromString(action?: string): ModlogActionType {
116 return action !== undefined ? (action as ModlogActionType) : "All";
119 const getModlogActionMapper =
121 actionType: ModlogActionType,
122 getAction: (view: View) => { id: number; when_: string }
124 (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
125 const { id, when_ } = getAction(view);
132 moderator: view.moderator ?? view.admin,
136 function buildCombined({
144 admin_purged_comments,
145 admin_purged_communities,
146 admin_purged_persons,
149 banned_from_community,
150 transferred_to_community,
151 }: GetModlogResponse): ModlogType[] {
152 const combined = removed_posts
154 getModlogActionMapper(
156 ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
161 getModlogActionMapper(
163 ({ mod_lock_post }: ModLockPostView) => mod_lock_post
169 getModlogActionMapper(
171 ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
176 removed_comments.map(
177 getModlogActionMapper(
179 ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
184 removed_communities.map(
185 getModlogActionMapper(
186 "ModRemoveCommunity",
187 ({ mod_remove_community }: ModRemoveCommunityView) =>
193 banned_from_community.map(
194 getModlogActionMapper(
195 "ModBanFromCommunity",
196 ({ mod_ban_from_community }: ModBanFromCommunityView) =>
197 mod_ban_from_community
202 added_to_community.map(
203 getModlogActionMapper(
205 ({ mod_add_community }: ModAddCommunityView) => mod_add_community
210 transferred_to_community.map(
211 getModlogActionMapper(
212 "ModTransferCommunity",
213 ({ mod_transfer_community }: ModTransferCommunityView) =>
214 mod_transfer_community
220 getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add)
225 getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban)
229 admin_purged_persons.map(
230 getModlogActionMapper(
232 ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
237 admin_purged_communities.map(
238 getModlogActionMapper(
239 "AdminPurgeCommunity",
240 ({ admin_purge_community }: AdminPurgeCommunityView) =>
241 admin_purge_community
246 admin_purged_posts.map(
247 getModlogActionMapper(
249 ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
254 admin_purged_comments.map(
255 getModlogActionMapper(
257 ({ admin_purge_comment }: AdminPurgeCommentView) =>
264 combined.sort((a, b) => b.when_.localeCompare(a.when_));
269 function renderModlogType({ type_, view }: ModlogType) {
271 case "ModRemovePost": {
272 const mrpv = view as ModRemovePostView;
274 mod_remove_post: { reason, removed },
280 <span>{removed ? "Removed " : "Restored "}</span>
282 Post <Link to={`/post/${id}`}>{name}</Link>
286 <div>reason: {reason}</div>
293 case "ModLockPost": {
295 mod_lock_post: { locked },
297 } = view as ModLockPostView;
301 <span>{locked ? "Locked " : "Unlocked "}</span>
303 Post <Link to={`/post/${id}`}>{name}</Link>
309 case "ModFeaturePost": {
311 mod_feature_post: { featured, is_featured_community },
313 } = view as ModFeaturePostView;
317 <span>{featured ? "Featured " : "Unfeatured "}</span>
319 Post <Link to={`/post/${id}`}>{name}</Link>
321 <span>{is_featured_community ? " In Community" : " In Local"}</span>
325 case "ModRemoveComment": {
326 const mrc = view as ModRemoveCommentView;
328 mod_remove_comment: { reason, removed },
329 comment: { id, content },
335 <span>{removed ? "Removed " : "Restored "}</span>
337 Comment <Link to={`/comment/${id}`}>{content}</Link>
341 by <PersonListing person={commenter} />
345 <div>reason: {reason}</div>
352 case "ModRemoveCommunity": {
353 const mrco = view as ModRemoveCommunityView;
355 mod_remove_community: { reason, expires, removed },
361 <span>{removed ? "Removed " : "Restored "}</span>
363 Community <CommunityLink community={community} />
367 <div>reason: {reason}</div>
372 <div>expires: {moment.utc(expires).fromNow()}</div>
379 case "ModBanFromCommunity": {
380 const mbfc = view as ModBanFromCommunityView;
382 mod_ban_from_community: { reason, expires, banned },
389 <span>{banned ? "Banned " : "Unbanned "}</span>
391 <PersonListing person={banned_person} />
393 <span> from the community </span>
395 <CommunityLink community={community} />
399 <div>reason: {reason}</div>
404 <div>expires: {moment.utc(expires).fromNow()}</div>
411 case "ModAddCommunity": {
413 mod_add_community: { removed },
416 } = view as ModAddCommunityView;
420 <span>{removed ? "Removed " : "Appointed "}</span>
422 <PersonListing person={modded_person} />
424 <span> as a mod to the community </span>
426 <CommunityLink community={community} />
432 case "ModTransferCommunity": {
433 const { community, modded_person } = view as ModTransferCommunityView;
437 <span>Transferred</span>
439 <CommunityLink community={community} />
443 <PersonListing person={modded_person} />
451 mod_ban: { reason, expires, banned },
453 } = view as ModBanView;
457 <span>{banned ? "Banned " : "Unbanned "}</span>
459 <PersonListing person={banned_person} />
463 <div>reason: {reason}</div>
468 <div>expires: {moment.utc(expires).fromNow()}</div>
477 mod_add: { removed },
479 } = view as ModAddView;
483 <span>{removed ? "Removed " : "Appointed "}</span>
485 <PersonListing person={modded_person} />
487 <span> as an admin </span>
491 case "AdminPurgePerson": {
493 admin_purge_person: { reason },
494 } = view as AdminPurgePersonView;
498 <span>Purged a Person</span>
501 <div>reason: {reason}</div>
508 case "AdminPurgeCommunity": {
510 admin_purge_community: { reason },
511 } = view as AdminPurgeCommunityView;
515 <span>Purged a Community</span>
518 <div>reason: {reason}</div>
525 case "AdminPurgePost": {
527 admin_purge_post: { reason },
529 } = view as AdminPurgePostView;
533 <span>Purged a Post from from </span>
534 <CommunityLink community={community} />
537 <div>reason: {reason}</div>
544 case "AdminPurgeComment": {
546 admin_purge_comment: { reason },
548 } = view as AdminPurgeCommentView;
553 Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
557 <div>reason: {reason}</div>
577 filterType: FilterType;
578 onChange: (option: Choice) => void;
579 value?: number | null;
580 onSearch: (text: string) => void;
584 <div className="col-sm-6 mb-3">
585 <label className="mb-2" htmlFor={`filter-${filterType}`}>
586 {i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
589 id={`filter-${filterType}`}
593 label: i18n.t("all"),
604 async function createNewOptions({
610 oldOptions: Choice[];
613 const newOptions: Choice[] = [];
616 const selectedUser = oldOptions.find(
617 ({ value }) => value === id.toString()
621 newOptions.push(selectedUser);
625 if (text.length > 0) {
627 ...(await fetchUsers(text))
628 .slice(0, Number(fetchLimit))
629 .map<Choice>(personToChoice)
636 export class Modlog extends Component<
637 RouteComponentProps<{ communityId?: string }>,
640 private isoData = setIsoData<ModlogData>(this.context);
642 state: ModlogState = {
643 res: { state: "empty" },
644 communityRes: { state: "empty" },
645 loadingModSearch: false,
646 loadingUserSearch: false,
647 userSearchOptions: [],
648 modSearchOptions: [],
652 props: RouteComponentProps<{ communityId?: string }>,
655 super(props, context);
656 this.handlePageChange = this.handlePageChange.bind(this);
657 this.handleUserChange = this.handleUserChange.bind(this);
658 this.handleModChange = this.handleModChange.bind(this);
660 // Only fetch the data if coming from another route
661 if (FirstLoadService.isFirstLoad) {
662 const { res, communityRes, modUserResponse, userResponse } =
663 this.isoData.routeData;
671 if (modUserResponse.state === "success") {
674 modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
678 if (userResponse.state === "success") {
681 userSearchOptions: [personToChoice(userResponse.data.person_view)],
688 const res = this.state.res;
689 const combined = res.state == "success" ? buildCombined(res.data) : [];
696 <MomentTime published={i.when_} />
699 {this.amAdminOrMod && i.moderator ? (
700 <PersonListing person={i.moderator} />
702 <div>{this.modOrAdminText(i.moderator)}</div>
705 <td>{renderModlogType(i)}</td>
712 get amAdminOrMod(): boolean {
714 this.state.communityRes.state == "success" &&
715 amMod(this.state.communityRes.data.moderators);
716 return amAdmin() || amMod_;
719 modOrAdminText(person?: Person): string {
721 this.isoData.site_res.admins.some(
722 ({ person: { id } }) => id === person.id
728 get documentTitle(): string {
729 return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
739 const { actionType, modId, userId } = getModlogQueryParams();
742 <div className="container-lg">
744 title={this.documentTitle}
745 path={this.context.router.route.match.url}
750 className="alert alert-warning text-sm-start text-xs-center"
754 icon="alert-triangle"
756 classes="me-sm-2 mx-auto d-sm-inline d-block"
758 <T i18nKey="modlog_content_warning" class="d-inline">
762 {this.state.communityRes.state === "success" && (
765 className="text-body"
766 to={`/c/${this.state.communityRes.data.community_view.community.name}`}
768 /c/{this.state.communityRes.data.community_view.community.name}{" "}
770 <span>{i18n.t("modlog")}</span>
773 <div className="row mb-2">
774 <div className="col-sm-6">
777 onChange={linkEvent(this, this.handleFilterActionChange)}
778 className="form-select"
781 <option disabled aria-hidden="true">
782 {i18n.t("filter_by_action")}
784 <option value={"All"}>{i18n.t("all")}</option>
785 <option value={"ModRemovePost"}>Removing Posts</option>
786 <option value={"ModLockPost"}>Locking Posts</option>
787 <option value={"ModFeaturePost"}>Featuring Posts</option>
788 <option value={"ModRemoveComment"}>Removing Comments</option>
789 <option value={"ModRemoveCommunity"}>
792 <option value={"ModBanFromCommunity"}>
793 Banning From Communities
795 <option value={"ModAddCommunity"}>
796 Adding Mod to Community
798 <option value={"ModTransferCommunity"}>
799 Transferring Communities
801 <option value={"ModAdd"}>Adding Mod to Site</option>
802 <option value={"ModBan"}>Banning From Site</option>
806 <div className="row mb-2">
809 onChange={this.handleUserChange}
810 onSearch={this.handleSearchUsers}
812 options={userSearchOptions}
813 loading={loadingUserSearch}
815 {!this.isoData.site_res.site_view.local_site
816 .hide_modlog_mod_names && (
819 onChange={this.handleModChange}
820 onSearch={this.handleSearchMods}
822 options={modSearchOptions}
823 loading={loadingModSearch}
827 {this.renderModlogTable()}
833 renderModlogTable() {
834 switch (this.state.res.state) {
842 const page = getModlogQueryParams().page;
844 <div className="table-responsive">
845 <table id="modlog_table" className="table table-sm table-hover">
846 <thead className="pointer">
848 <th> {i18n.t("time")}</th>
849 <th>{i18n.t("mod")}</th>
850 <th>{i18n.t("action")}</th>
855 <Paginator page={page} onChange={this.handlePageChange} />
862 handleFilterActionChange(i: Modlog, event: any) {
864 actionType: event.target.value as ModlogActionType,
869 handlePageChange(page: number) {
870 this.updateUrl({ page });
873 handleUserChange(option: Choice) {
874 this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
877 handleModChange(option: Choice) {
878 this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
881 handleSearchUsers = debounce(async (text: string) => {
882 const { userId } = getModlogQueryParams();
883 const { userSearchOptions } = this.state;
884 this.setState({ loadingUserSearch: true });
886 const newOptions = await createNewOptions({
889 oldOptions: userSearchOptions,
893 userSearchOptions: newOptions,
894 loadingUserSearch: false,
898 handleSearchMods = debounce(async (text: string) => {
899 const { modId } = getModlogQueryParams();
900 const { modSearchOptions } = this.state;
901 this.setState({ loadingModSearch: true });
903 const newOptions = await createNewOptions({
906 oldOptions: modSearchOptions,
910 modSearchOptions: newOptions,
911 loadingModSearch: false,
915 async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
918 actionType: urlActionType,
921 } = getModlogQueryParams();
923 const queryParams: QueryParams<ModlogProps> = {
924 page: (page ?? urlPage).toString(),
925 actionType: actionType ?? urlActionType,
926 modId: getUpdatedSearchId(modId, urlModId),
927 userId: getUpdatedSearchId(userId, urlUserId),
930 const communityId = this.props.match.params.communityId;
932 this.props.history.push(
933 `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
938 await this.refetch();
942 const auth = myAuth();
943 const { actionType, page, modId, userId } = getModlogQueryParams();
944 const { communityId: urlCommunityId } = this.props.match.params;
945 const communityId = getIdFromString(urlCommunityId);
947 this.setState({ res: { state: "loading" } });
949 res: await HttpService.client.getModlog({
950 community_id: communityId,
954 other_person_id: userId ?? undefined,
955 mod_person_id: !this.isoData.site_res.site_view.local_site
956 .hide_modlog_mod_names
964 this.setState({ communityRes: { state: "loading" } });
966 communityRes: await HttpService.client.getCommunity({
974 static async fetchInitialData({
977 query: { modId: urlModId, page, userId: urlUserId, actionType },
980 }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
981 const pathSplit = path.split("/");
982 const communityId = getIdFromString(pathSplit[2]);
983 const modId = !site.site_view.local_site.hide_modlog_mod_names
984 ? getIdFromString(urlModId)
986 const userId = getIdFromString(urlUserId);
988 const modlogForm: GetModlog = {
989 page: getPageFromString(page),
991 community_id: communityId,
992 type_: getActionFromString(actionType),
993 mod_person_id: modId,
994 other_person_id: userId,
998 let communityResponse: RequestState<GetCommunityResponse> = {
1003 const communityForm: GetCommunity = {
1008 communityResponse = await client.getCommunity(communityForm);
1011 let modUserResponse: RequestState<GetPersonDetailsResponse> = {
1016 const getPersonForm: GetPersonDetails = {
1021 modUserResponse = await client.getPersonDetails(getPersonForm);
1024 let userResponse: RequestState<GetPersonDetailsResponse> = {
1029 const getPersonForm: GetPersonDetails = {
1034 userResponse = await client.getPersonDetails(getPersonForm);
1038 res: await client.getModlog(modlogForm),
1039 communityRes: communityResponse,