1 import { NoOptionI18nKeys } from "i18next";
2 import { Component, linkEvent } from "inferno";
3 import { T } from "inferno-i18next-dess";
4 import { Link } from "inferno-router";
5 import { RouteComponentProps } from "inferno-router/dist/Route";
8 AdminPurgeCommunityView,
16 GetPersonDetailsResponse,
19 ModBanFromCommunityView,
24 ModRemoveCommunityView,
26 ModTransferCommunityView,
29 } from "lemmy-js-client";
30 import moment from "moment";
31 import { i18n } from "../i18next";
32 import { InitialFetchRequest } from "../interfaces";
33 import { FirstLoadService } from "../services/FirstLoadService";
34 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 const getModlogActionMapper =
124 actionType: ModlogActionType,
125 getAction: (view: View) => { id: number; when_: string }
127 (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
128 const { id, when_ } = getAction(view);
135 moderator: view.moderator ?? view.admin,
139 function buildCombined({
147 admin_purged_comments,
148 admin_purged_communities,
149 admin_purged_persons,
152 banned_from_community,
153 transferred_to_community,
154 }: GetModlogResponse): ModlogType[] {
155 const combined = removed_posts
157 getModlogActionMapper(
159 ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
164 getModlogActionMapper(
166 ({ mod_lock_post }: ModLockPostView) => mod_lock_post
172 getModlogActionMapper(
174 ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
179 removed_comments.map(
180 getModlogActionMapper(
182 ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
187 removed_communities.map(
188 getModlogActionMapper(
189 "ModRemoveCommunity",
190 ({ mod_remove_community }: ModRemoveCommunityView) =>
196 banned_from_community.map(
197 getModlogActionMapper(
198 "ModBanFromCommunity",
199 ({ mod_ban_from_community }: ModBanFromCommunityView) =>
200 mod_ban_from_community
205 added_to_community.map(
206 getModlogActionMapper(
208 ({ mod_add_community }: ModAddCommunityView) => mod_add_community
213 transferred_to_community.map(
214 getModlogActionMapper(
215 "ModTransferCommunity",
216 ({ mod_transfer_community }: ModTransferCommunityView) =>
217 mod_transfer_community
223 getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add)
228 getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban)
232 admin_purged_persons.map(
233 getModlogActionMapper(
235 ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
240 admin_purged_communities.map(
241 getModlogActionMapper(
242 "AdminPurgeCommunity",
243 ({ admin_purge_community }: AdminPurgeCommunityView) =>
244 admin_purge_community
249 admin_purged_posts.map(
250 getModlogActionMapper(
252 ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
257 admin_purged_comments.map(
258 getModlogActionMapper(
260 ({ admin_purge_comment }: AdminPurgeCommentView) =>
267 combined.sort((a, b) => b.when_.localeCompare(a.when_));
272 function renderModlogType({ type_, view }: ModlogType) {
274 case "ModRemovePost": {
275 const mrpv = view as ModRemovePostView;
277 mod_remove_post: { reason, removed },
283 <span>{removed ? "Removed " : "Restored "}</span>
285 Post <Link to={`/post/${id}`}>{name}</Link>
289 <div>reason: {reason}</div>
296 case "ModLockPost": {
298 mod_lock_post: { locked },
300 } = view as ModLockPostView;
304 <span>{locked ? "Locked " : "Unlocked "}</span>
306 Post <Link to={`/post/${id}`}>{name}</Link>
312 case "ModFeaturePost": {
314 mod_feature_post: { featured, is_featured_community },
316 } = view as ModFeaturePostView;
320 <span>{featured ? "Featured " : "Unfeatured "}</span>
322 Post <Link to={`/post/${id}`}>{name}</Link>
324 <span>{is_featured_community ? " In Community" : " In Local"}</span>
328 case "ModRemoveComment": {
329 const mrc = view as ModRemoveCommentView;
331 mod_remove_comment: { reason, removed },
332 comment: { id, content },
338 <span>{removed ? "Removed " : "Restored "}</span>
340 Comment <Link to={`/comment/${id}`}>{content}</Link>
344 by <PersonListing person={commenter} />
348 <div>reason: {reason}</div>
355 case "ModRemoveCommunity": {
356 const mrco = view as ModRemoveCommunityView;
358 mod_remove_community: { reason, expires, removed },
364 <span>{removed ? "Removed " : "Restored "}</span>
366 Community <CommunityLink community={community} />
370 <div>reason: {reason}</div>
375 <div>expires: {moment.utc(expires).fromNow()}</div>
382 case "ModBanFromCommunity": {
383 const mbfc = view as ModBanFromCommunityView;
385 mod_ban_from_community: { reason, expires, banned },
392 <span>{banned ? "Banned " : "Unbanned "}</span>
394 <PersonListing person={banned_person} />
396 <span> from the community </span>
398 <CommunityLink community={community} />
402 <div>reason: {reason}</div>
407 <div>expires: {moment.utc(expires).fromNow()}</div>
414 case "ModAddCommunity": {
416 mod_add_community: { removed },
419 } = view as ModAddCommunityView;
423 <span>{removed ? "Removed " : "Appointed "}</span>
425 <PersonListing person={modded_person} />
427 <span> as a mod to the community </span>
429 <CommunityLink community={community} />
435 case "ModTransferCommunity": {
436 const { community, modded_person } = view as ModTransferCommunityView;
440 <span>Transferred</span>
442 <CommunityLink community={community} />
446 <PersonListing person={modded_person} />
454 mod_ban: { reason, expires, banned },
456 } = view as ModBanView;
460 <span>{banned ? "Banned " : "Unbanned "}</span>
462 <PersonListing person={banned_person} />
466 <div>reason: {reason}</div>
471 <div>expires: {moment.utc(expires).fromNow()}</div>
480 mod_add: { removed },
482 } = view as ModAddView;
486 <span>{removed ? "Removed " : "Appointed "}</span>
488 <PersonListing person={modded_person} />
490 <span> as an admin </span>
494 case "AdminPurgePerson": {
496 admin_purge_person: { reason },
497 } = view as AdminPurgePersonView;
501 <span>Purged a Person</span>
504 <div>reason: {reason}</div>
511 case "AdminPurgeCommunity": {
513 admin_purge_community: { reason },
514 } = view as AdminPurgeCommunityView;
518 <span>Purged a Community</span>
521 <div>reason: {reason}</div>
528 case "AdminPurgePost": {
530 admin_purge_post: { reason },
532 } = view as AdminPurgePostView;
536 <span>Purged a Post from from </span>
537 <CommunityLink community={community} />
540 <div>reason: {reason}</div>
547 case "AdminPurgeComment": {
549 admin_purge_comment: { reason },
551 } = view as AdminPurgeCommentView;
556 Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
560 <div>reason: {reason}</div>
580 filterType: FilterType;
581 onChange: (option: Choice) => void;
582 value?: number | null;
583 onSearch: (text: string) => void;
587 <div className="col-sm-6 mb-3">
588 <label className="mb-2" htmlFor={`filter-${filterType}`}>
589 {i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
592 id={`filter-${filterType}`}
596 label: i18n.t("all"),
607 async function createNewOptions({
613 oldOptions: Choice[];
616 const newOptions: Choice[] = [];
619 const selectedUser = oldOptions.find(
620 ({ value }) => value === id.toString()
624 newOptions.push(selectedUser);
628 if (text.length > 0) {
630 ...(await fetchUsers(text))
631 .slice(0, Number(fetchLimit))
632 .map<Choice>(personToChoice)
639 export class Modlog extends Component<
640 RouteComponentProps<{ communityId?: string }>,
643 private isoData = setIsoData<ModlogData>(this.context);
645 state: ModlogState = {
646 res: { state: "empty" },
647 communityRes: { state: "empty" },
648 loadingModSearch: false,
649 loadingUserSearch: false,
650 userSearchOptions: [],
651 modSearchOptions: [],
655 props: RouteComponentProps<{ communityId?: string }>,
658 super(props, context);
659 this.handlePageChange = this.handlePageChange.bind(this);
660 this.handleUserChange = this.handleUserChange.bind(this);
661 this.handleModChange = this.handleModChange.bind(this);
663 // Only fetch the data if coming from another route
664 if (FirstLoadService.isFirstLoad) {
665 const { res, communityRes, modUserResponse, userResponse } =
666 this.isoData.routeData;
674 if (modUserResponse.state === "success") {
677 modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
681 if (userResponse.state === "success") {
684 userSearchOptions: [personToChoice(userResponse.data.person_view)],
691 const res = this.state.res;
692 const combined = res.state == "success" ? buildCombined(res.data) : [];
699 <MomentTime published={i.when_} />
702 {this.amAdminOrMod && i.moderator ? (
703 <PersonListing person={i.moderator} />
705 <div>{this.modOrAdminText(i.moderator)}</div>
708 <td>{renderModlogType(i)}</td>
715 get amAdminOrMod(): boolean {
717 this.state.communityRes.state == "success" &&
718 amMod(this.state.communityRes.data.moderators);
719 return amAdmin() || amMod_;
722 modOrAdminText(person?: Person): string {
724 this.isoData.site_res.admins.some(
725 ({ person: { id } }) => id === person.id
731 get documentTitle(): string {
732 return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
742 const { actionType, modId, userId } = getModlogQueryParams();
745 <div className="modlog container-lg">
747 title={this.documentTitle}
748 path={this.context.router.route.match.url}
753 className="alert alert-warning text-sm-start text-xs-center"
757 icon="alert-triangle"
759 classes="me-sm-2 mx-auto d-sm-inline d-block"
761 <T i18nKey="modlog_content_warning" class="d-inline">
765 {this.state.communityRes.state === "success" && (
768 className="text-body"
769 to={`/c/${this.state.communityRes.data.community_view.community.name}`}
771 /c/{this.state.communityRes.data.community_view.community.name}{" "}
773 <span>{i18n.t("modlog")}</span>
776 <div className="row mb-2">
777 <div className="col-sm-6">
780 onChange={linkEvent(this, this.handleFilterActionChange)}
781 className="form-select"
784 <option disabled aria-hidden="true">
785 {i18n.t("filter_by_action")}
787 <option value={"All"}>{i18n.t("all")}</option>
788 <option value={"ModRemovePost"}>Removing Posts</option>
789 <option value={"ModLockPost"}>Locking Posts</option>
790 <option value={"ModFeaturePost"}>Featuring Posts</option>
791 <option value={"ModRemoveComment"}>Removing Comments</option>
792 <option value={"ModRemoveCommunity"}>
795 <option value={"ModBanFromCommunity"}>
796 Banning From Communities
798 <option value={"ModAddCommunity"}>
799 Adding Mod to Community
801 <option value={"ModTransferCommunity"}>
802 Transferring Communities
804 <option value={"ModAdd"}>Adding Mod to Site</option>
805 <option value={"ModBan"}>Banning From Site</option>
809 <div className="row mb-2">
812 onChange={this.handleUserChange}
813 onSearch={this.handleSearchUsers}
815 options={userSearchOptions}
816 loading={loadingUserSearch}
818 {!this.isoData.site_res.site_view.local_site
819 .hide_modlog_mod_names && (
822 onChange={this.handleModChange}
823 onSearch={this.handleSearchMods}
825 options={modSearchOptions}
826 loading={loadingModSearch}
830 {this.renderModlogTable()}
836 renderModlogTable() {
837 switch (this.state.res.state) {
845 const page = getModlogQueryParams().page;
847 <div className="table-responsive">
848 <table id="modlog_table" className="table table-sm table-hover">
849 <thead className="pointer">
851 <th> {i18n.t("time")}</th>
852 <th>{i18n.t("mod")}</th>
853 <th>{i18n.t("action")}</th>
858 <Paginator page={page} onChange={this.handlePageChange} />
865 handleFilterActionChange(i: Modlog, event: any) {
867 actionType: event.target.value as ModlogActionType,
872 handlePageChange(page: number) {
873 this.updateUrl({ page });
876 handleUserChange(option: Choice) {
877 this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
880 handleModChange(option: Choice) {
881 this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
884 handleSearchUsers = debounce(async (text: string) => {
885 const { userId } = getModlogQueryParams();
886 const { userSearchOptions } = this.state;
887 this.setState({ loadingUserSearch: true });
889 const newOptions = await createNewOptions({
892 oldOptions: userSearchOptions,
896 userSearchOptions: newOptions,
897 loadingUserSearch: false,
901 handleSearchMods = debounce(async (text: string) => {
902 const { modId } = getModlogQueryParams();
903 const { modSearchOptions } = this.state;
904 this.setState({ loadingModSearch: true });
906 const newOptions = await createNewOptions({
909 oldOptions: modSearchOptions,
913 modSearchOptions: newOptions,
914 loadingModSearch: false,
918 async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
921 actionType: urlActionType,
924 } = getModlogQueryParams();
926 const queryParams: QueryParams<ModlogProps> = {
927 page: (page ?? urlPage).toString(),
928 actionType: actionType ?? urlActionType,
929 modId: getUpdatedSearchId(modId, urlModId),
930 userId: getUpdatedSearchId(userId, urlUserId),
933 const communityId = this.props.match.params.communityId;
935 this.props.history.push(
936 `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
941 await this.refetch();
945 const auth = myAuth();
946 const { actionType, page, modId, userId } = getModlogQueryParams();
947 const { communityId: urlCommunityId } = this.props.match.params;
948 const communityId = getIdFromString(urlCommunityId);
950 this.setState({ res: { state: "loading" } });
952 res: await HttpService.client.getModlog({
953 community_id: communityId,
957 other_person_id: userId ?? undefined,
958 mod_person_id: !this.isoData.site_res.site_view.local_site
959 .hide_modlog_mod_names
967 this.setState({ communityRes: { state: "loading" } });
969 communityRes: await HttpService.client.getCommunity({
977 static async fetchInitialData({
980 query: { modId: urlModId, page, userId: urlUserId, actionType },
983 }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
984 const pathSplit = path.split("/");
985 const communityId = getIdFromString(pathSplit[2]);
986 const modId = !site.site_view.local_site.hide_modlog_mod_names
987 ? getIdFromString(urlModId)
989 const userId = getIdFromString(urlUserId);
991 const modlogForm: GetModlog = {
992 page: getPageFromString(page),
994 community_id: communityId,
995 type_: getActionFromString(actionType),
996 mod_person_id: modId,
997 other_person_id: userId,
1001 let communityResponse: RequestState<GetCommunityResponse> = {
1006 const communityForm: GetCommunity = {
1011 communityResponse = await client.getCommunity(communityForm);
1014 let modUserResponse: RequestState<GetPersonDetailsResponse> = {
1019 const getPersonForm: GetPersonDetails = {
1024 modUserResponse = await client.getPersonDetails(getPersonForm);
1027 let userResponse: RequestState<GetPersonDetailsResponse> = {
1032 const getPersonForm: GetPersonDetails = {
1037 userResponse = await client.getPersonDetails(getPersonForm);
1041 res: await client.getModlog(modlogForm),
1042 communityRes: communityResponse,