14 } from "@utils/helpers";
15 import { amAdmin, amMod } from "@utils/roles";
16 import type { QueryParams } from "@utils/types";
17 import { Choice, RouteDataResponse } from "@utils/types";
18 import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
19 import parseISO from "date-fns/parseISO";
20 import { NoOptionI18nKeys } from "i18next";
21 import { Component, linkEvent } from "inferno";
22 import { T } from "inferno-i18next-dess";
23 import { Link } from "inferno-router";
24 import { RouteComponentProps } from "inferno-router/dist/Route";
26 AdminPurgeCommentView,
27 AdminPurgeCommunityView,
35 GetPersonDetailsResponse,
38 ModBanFromCommunityView,
43 ModRemoveCommunityView,
45 ModTransferCommunityView,
48 } from "lemmy-js-client";
49 import { fetchLimit } from "../config";
50 import { InitialFetchRequest } from "../interfaces";
51 import { FirstLoadService, I18NextService } from "../services";
52 import { HttpService, RequestState } from "../services/HttpService";
53 import { HtmlTags } from "./common/html-tags";
54 import { Icon, Spinner } from "./common/icon";
55 import { MomentTime } from "./common/moment-time";
56 import { Paginator } from "./common/paginator";
57 import { SearchableSelect } from "./common/searchable-select";
58 import { CommunityLink } from "./community/community-link";
59 import { PersonListing } from "./person/person-listing";
61 type FilterType = "mod" | "user";
67 | ModRemoveCommentView
68 | ModRemoveCommunityView
69 | ModBanFromCommunityView
72 | ModTransferCommunityView
74 | AdminPurgePersonView
75 | AdminPurgeCommunityView
77 | AdminPurgeCommentView;
79 type ModlogData = RouteDataResponse<{
80 res: GetModlogResponse;
81 communityRes: GetCommunityResponse;
82 modUserResponse: GetPersonDetailsResponse;
83 userResponse: GetPersonDetailsResponse;
86 interface ModlogType {
88 type_: ModlogActionType;
94 const getModlogQueryParams = () =>
95 getQueryParams<ModlogProps>({
96 actionType: getActionFromString,
97 modId: getIdFromString,
98 userId: getIdFromString,
99 page: getPageFromString,
102 interface ModlogState {
103 res: RequestState<GetModlogResponse>;
104 communityRes: RequestState<GetCommunityResponse>;
105 loadingModSearch: boolean;
106 loadingUserSearch: boolean;
107 modSearchOptions: Choice[];
108 userSearchOptions: Choice[];
111 interface ModlogProps {
113 userId?: number | null;
114 modId?: number | null;
115 actionType: ModlogActionType;
118 function getActionFromString(action?: string): ModlogActionType {
119 return action !== undefined ? (action as ModlogActionType) : "All";
122 function getExpires(expires: string) {
123 return formatDistanceToNowStrict(parseISO(expires));
126 const getModlogActionMapper =
128 actionType: ModlogActionType,
129 getAction: (view: View) => { id: number; when_: string }
131 (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
132 const { id, when_ } = getAction(view);
139 moderator: view.moderator ?? view.admin,
143 function buildCombined({
151 admin_purged_comments,
152 admin_purged_communities,
153 admin_purged_persons,
156 banned_from_community,
157 transferred_to_community,
158 }: GetModlogResponse): ModlogType[] {
159 const combined = removed_posts
161 getModlogActionMapper(
163 ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
168 getModlogActionMapper(
170 ({ mod_lock_post }: ModLockPostView) => mod_lock_post
176 getModlogActionMapper(
178 ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
183 removed_comments.map(
184 getModlogActionMapper(
186 ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
191 removed_communities.map(
192 getModlogActionMapper(
193 "ModRemoveCommunity",
194 ({ mod_remove_community }: ModRemoveCommunityView) =>
200 banned_from_community.map(
201 getModlogActionMapper(
202 "ModBanFromCommunity",
203 ({ mod_ban_from_community }: ModBanFromCommunityView) =>
204 mod_ban_from_community
209 added_to_community.map(
210 getModlogActionMapper(
212 ({ mod_add_community }: ModAddCommunityView) => mod_add_community
217 transferred_to_community.map(
218 getModlogActionMapper(
219 "ModTransferCommunity",
220 ({ mod_transfer_community }: ModTransferCommunityView) =>
221 mod_transfer_community
227 getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add)
232 getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban)
236 admin_purged_persons.map(
237 getModlogActionMapper(
239 ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
244 admin_purged_communities.map(
245 getModlogActionMapper(
246 "AdminPurgeCommunity",
247 ({ admin_purge_community }: AdminPurgeCommunityView) =>
248 admin_purge_community
253 admin_purged_posts.map(
254 getModlogActionMapper(
256 ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
261 admin_purged_comments.map(
262 getModlogActionMapper(
264 ({ admin_purge_comment }: AdminPurgeCommentView) =>
271 combined.sort((a, b) => b.when_.localeCompare(a.when_));
276 function renderModlogType({ type_, view }: ModlogType) {
278 case "ModRemovePost": {
279 const mrpv = view as ModRemovePostView;
281 mod_remove_post: { reason, removed },
287 <span>{removed ? "Removed " : "Restored "}</span>
289 Post <Link to={`/post/${id}`}>{name}</Link>
293 <div>reason: {reason}</div>
300 case "ModLockPost": {
302 mod_lock_post: { locked },
304 } = view as ModLockPostView;
308 <span>{locked ? "Locked " : "Unlocked "}</span>
310 Post <Link to={`/post/${id}`}>{name}</Link>
316 case "ModFeaturePost": {
318 mod_feature_post: { featured, is_featured_community },
320 } = view as ModFeaturePostView;
324 <span>{featured ? "Featured " : "Unfeatured "}</span>
326 Post <Link to={`/post/${id}`}>{name}</Link>
328 <span>{is_featured_community ? " In Community" : " In Local"}</span>
332 case "ModRemoveComment": {
333 const mrc = view as ModRemoveCommentView;
335 mod_remove_comment: { reason, removed },
336 comment: { id, content },
342 <span>{removed ? "Removed " : "Restored "}</span>
344 Comment <Link to={`/comment/${id}`}>{content}</Link>
348 by <PersonListing person={commenter} />
352 <div>reason: {reason}</div>
359 case "ModRemoveCommunity": {
360 const mrco = view as ModRemoveCommunityView;
362 mod_remove_community: { reason, expires, removed },
368 <span>{removed ? "Removed " : "Restored "}</span>
370 Community <CommunityLink community={community} />
374 <div>reason: {reason}</div>
379 <div>expires: {getExpires(expires)}</div>
386 case "ModBanFromCommunity": {
387 const mbfc = view as ModBanFromCommunityView;
389 mod_ban_from_community: { reason, expires, banned },
396 <span>{banned ? "Banned " : "Unbanned "}</span>
398 <PersonListing person={banned_person} />
400 <span> from the community </span>
402 <CommunityLink community={community} />
406 <div>reason: {reason}</div>
411 <div>expires: {getExpires(expires)}</div>
418 case "ModAddCommunity": {
420 mod_add_community: { removed },
423 } = view as ModAddCommunityView;
427 <span>{removed ? "Removed " : "Appointed "}</span>
429 <PersonListing person={modded_person} />
431 <span> as a mod to the community </span>
433 <CommunityLink community={community} />
439 case "ModTransferCommunity": {
440 const { community, modded_person } = view as ModTransferCommunityView;
444 <span>Transferred</span>
446 <CommunityLink community={community} />
450 <PersonListing person={modded_person} />
458 mod_ban: { reason, expires, banned },
460 } = view as ModBanView;
464 <span>{banned ? "Banned " : "Unbanned "}</span>
466 <PersonListing person={banned_person} />
470 <div>reason: {reason}</div>
475 <div>expires: {getExpires(expires)}</div>
484 mod_add: { removed },
486 } = view as ModAddView;
490 <span>{removed ? "Removed " : "Appointed "}</span>
492 <PersonListing person={modded_person} />
494 <span> as an admin </span>
498 case "AdminPurgePerson": {
500 admin_purge_person: { reason },
501 } = view as AdminPurgePersonView;
505 <span>Purged a Person</span>
508 <div>reason: {reason}</div>
515 case "AdminPurgeCommunity": {
517 admin_purge_community: { reason },
518 } = view as AdminPurgeCommunityView;
522 <span>Purged a Community</span>
525 <div>reason: {reason}</div>
532 case "AdminPurgePost": {
534 admin_purge_post: { reason },
536 } = view as AdminPurgePostView;
540 <span>Purged a Post from from </span>
541 <CommunityLink community={community} />
544 <div>reason: {reason}</div>
551 case "AdminPurgeComment": {
553 admin_purge_comment: { reason },
555 } = view as AdminPurgeCommentView;
560 Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
564 <div>reason: {reason}</div>
584 filterType: FilterType;
585 onChange: (option: Choice) => void;
586 value?: number | null;
587 onSearch: (text: string) => void;
591 <div className="col-sm-6 mb-3">
592 <label className="mb-2" htmlFor={`filter-${filterType}`}>
593 {I18NextService.i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
596 id={`filter-${filterType}`}
600 label: I18NextService.i18n.t("all"),
611 async function createNewOptions({
617 oldOptions: Choice[];
620 const newOptions: Choice[] = [];
623 const selectedUser = oldOptions.find(
624 ({ value }) => value === id.toString()
628 newOptions.push(selectedUser);
632 if (text.length > 0) {
634 ...(await fetchUsers(text))
635 .slice(0, Number(fetchLimit))
636 .map<Choice>(personToChoice)
643 export class Modlog extends Component<
644 RouteComponentProps<{ communityId?: string }>,
647 private isoData = setIsoData<ModlogData>(this.context);
649 state: ModlogState = {
650 res: { state: "empty" },
651 communityRes: { state: "empty" },
652 loadingModSearch: false,
653 loadingUserSearch: false,
654 userSearchOptions: [],
655 modSearchOptions: [],
659 props: RouteComponentProps<{ communityId?: string }>,
662 super(props, context);
663 this.handlePageChange = this.handlePageChange.bind(this);
664 this.handleUserChange = this.handleUserChange.bind(this);
665 this.handleModChange = this.handleModChange.bind(this);
667 // Only fetch the data if coming from another route
668 if (FirstLoadService.isFirstLoad) {
669 const { res, communityRes, modUserResponse, userResponse } =
670 this.isoData.routeData;
678 if (modUserResponse.state === "success") {
681 modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
685 if (userResponse.state === "success") {
688 userSearchOptions: [personToChoice(userResponse.data.person_view)],
695 const res = this.state.res;
696 const combined = res.state == "success" ? buildCombined(res.data) : [];
703 <MomentTime published={i.when_} />
706 {this.amAdminOrMod && i.moderator ? (
707 <PersonListing person={i.moderator} />
709 <div>{this.modOrAdminText(i.moderator)}</div>
712 <td>{renderModlogType(i)}</td>
719 get amAdminOrMod(): boolean {
721 this.state.communityRes.state == "success" &&
722 amMod(this.state.communityRes.data.moderators);
723 return amAdmin() || amMod_;
726 modOrAdminText(person?: Person): string {
728 this.isoData.site_res.admins.some(
729 ({ person: { id } }) => id === person.id
731 ? I18NextService.i18n.t("admin")
732 : I18NextService.i18n.t("mod");
735 get documentTitle(): string {
736 return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
746 const { actionType, modId, userId } = getModlogQueryParams();
749 <div className="modlog container-lg">
751 title={this.documentTitle}
752 path={this.context.router.route.match.url}
757 className="alert alert-warning text-sm-start text-xs-center"
761 icon="alert-triangle"
763 classes="me-sm-2 mx-auto d-sm-inline d-block"
765 <T i18nKey="modlog_content_warning" class="d-inline">
769 {this.state.communityRes.state === "success" && (
772 className="text-body"
773 to={`/c/${this.state.communityRes.data.community_view.community.name}`}
775 /c/{this.state.communityRes.data.community_view.community.name}{" "}
777 <span>{I18NextService.i18n.t("modlog")}</span>
780 <div className="row mb-2">
781 <div className="col-sm-6">
784 onChange={linkEvent(this, this.handleFilterActionChange)}
785 className="form-select"
788 <option disabled aria-hidden="true">
789 {I18NextService.i18n.t("filter_by_action")}
791 <option value={"All"}>{I18NextService.i18n.t("all")}</option>
792 <option value={"ModRemovePost"}>Removing Posts</option>
793 <option value={"ModLockPost"}>Locking Posts</option>
794 <option value={"ModFeaturePost"}>Featuring Posts</option>
795 <option value={"ModRemoveComment"}>Removing Comments</option>
796 <option value={"ModRemoveCommunity"}>
799 <option value={"ModBanFromCommunity"}>
800 Banning From Communities
802 <option value={"ModAddCommunity"}>
803 Adding Mod to Community
805 <option value={"ModTransferCommunity"}>
806 Transferring Communities
808 <option value={"ModAdd"}>Adding Mod to Site</option>
809 <option value={"ModBan"}>Banning From Site</option>
813 <div className="row mb-2">
816 onChange={this.handleUserChange}
817 onSearch={this.handleSearchUsers}
819 options={userSearchOptions}
820 loading={loadingUserSearch}
822 {!this.isoData.site_res.site_view.local_site
823 .hide_modlog_mod_names && (
826 onChange={this.handleModChange}
827 onSearch={this.handleSearchMods}
829 options={modSearchOptions}
830 loading={loadingModSearch}
834 {this.renderModlogTable()}
840 renderModlogTable() {
841 switch (this.state.res.state) {
849 const page = getModlogQueryParams().page;
851 <div className="table-responsive">
852 <table id="modlog_table" className="table table-sm table-hover">
853 <thead className="pointer">
855 <th> {I18NextService.i18n.t("time")}</th>
856 <th>{I18NextService.i18n.t("mod")}</th>
857 <th>{I18NextService.i18n.t("action")}</th>
862 <Paginator page={page} onChange={this.handlePageChange} />
869 handleFilterActionChange(i: Modlog, event: any) {
871 actionType: event.target.value as ModlogActionType,
876 handlePageChange(page: number) {
877 this.updateUrl({ page });
880 handleUserChange(option: Choice) {
881 this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
884 handleModChange(option: Choice) {
885 this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
888 handleSearchUsers = debounce(async (text: string) => {
889 const { userId } = getModlogQueryParams();
890 const { userSearchOptions } = this.state;
891 this.setState({ loadingUserSearch: true });
893 const newOptions = await createNewOptions({
896 oldOptions: userSearchOptions,
900 userSearchOptions: newOptions,
901 loadingUserSearch: false,
905 handleSearchMods = debounce(async (text: string) => {
906 const { modId } = getModlogQueryParams();
907 const { modSearchOptions } = this.state;
908 this.setState({ loadingModSearch: true });
910 const newOptions = await createNewOptions({
913 oldOptions: modSearchOptions,
917 modSearchOptions: newOptions,
918 loadingModSearch: false,
922 async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
925 actionType: urlActionType,
928 } = getModlogQueryParams();
930 const queryParams: QueryParams<ModlogProps> = {
931 page: (page ?? urlPage).toString(),
932 actionType: actionType ?? urlActionType,
933 modId: getUpdatedSearchId(modId, urlModId),
934 userId: getUpdatedSearchId(userId, urlUserId),
937 const communityId = this.props.match.params.communityId;
939 this.props.history.push(
940 `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
945 await this.refetch();
949 const auth = myAuth();
950 const { actionType, page, modId, userId } = getModlogQueryParams();
951 const { communityId: urlCommunityId } = this.props.match.params;
952 const communityId = getIdFromString(urlCommunityId);
954 this.setState({ res: { state: "loading" } });
956 res: await HttpService.client.getModlog({
957 community_id: communityId,
961 other_person_id: userId ?? undefined,
962 mod_person_id: !this.isoData.site_res.site_view.local_site
963 .hide_modlog_mod_names
971 this.setState({ communityRes: { state: "loading" } });
973 communityRes: await HttpService.client.getCommunity({
981 static async fetchInitialData({
984 query: { modId: urlModId, page, userId: urlUserId, actionType },
987 }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
988 const pathSplit = path.split("/");
989 const communityId = getIdFromString(pathSplit[2]);
990 const modId = !site.site_view.local_site.hide_modlog_mod_names
991 ? getIdFromString(urlModId)
993 const userId = getIdFromString(urlUserId);
995 const modlogForm: GetModlog = {
996 page: getPageFromString(page),
998 community_id: communityId,
999 type_: getActionFromString(actionType),
1000 mod_person_id: modId,
1001 other_person_id: userId,
1005 let communityResponse: RequestState<GetCommunityResponse> = {
1010 const communityForm: GetCommunity = {
1015 communityResponse = await client.getCommunity(communityForm);
1018 let modUserResponse: RequestState<GetPersonDetailsResponse> = {
1023 const getPersonForm: GetPersonDetails = {
1028 modUserResponse = await client.getPersonDetails(getPersonForm);
1031 let userResponse: RequestState<GetPersonDetailsResponse> = {
1036 const getPersonForm: GetPersonDetails = {
1041 userResponse = await client.getPersonDetails(getPersonForm);
1045 res: await client.getModlog(modlogForm),
1046 communityRes: communityResponse,