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,
18 ModBanFromCommunityView,
23 ModRemoveCommunityView,
25 ModTransferCommunityView,
28 } from "lemmy-js-client";
29 import moment from "moment";
30 import { i18n } from "../i18next";
31 import { InitialFetchRequest } from "../interfaces";
32 import { FirstLoadService } from "../services/FirstLoadService";
33 import { HttpService, RequestState } from "../services/HttpService";
51 import { HtmlTags } from "./common/html-tags";
52 import { Icon, Spinner } from "./common/icon";
53 import { MomentTime } from "./common/moment-time";
54 import { Paginator } from "./common/paginator";
55 import { SearchableSelect } from "./common/searchable-select";
56 import { CommunityLink } from "./community/community-link";
57 import { PersonListing } from "./person/person-listing";
59 type FilterType = "mod" | "user";
65 | ModRemoveCommentView
66 | ModRemoveCommunityView
67 | ModBanFromCommunityView
70 | ModTransferCommunityView
72 | AdminPurgePersonView
73 | AdminPurgeCommunityView
75 | AdminPurgeCommentView;
77 interface ModlogType {
79 type_: ModlogActionType;
85 const getModlogQueryParams = () =>
86 getQueryParams<ModlogProps>({
87 actionType: getActionFromString,
88 modId: getIdFromString,
89 userId: getIdFromString,
90 page: getPageFromString,
93 interface ModlogState {
94 res: RequestState<GetModlogResponse>;
95 communityRes: RequestState<GetCommunityResponse>;
96 loadingModSearch: boolean;
97 loadingUserSearch: boolean;
98 modSearchOptions: Choice[];
99 userSearchOptions: Choice[];
102 interface ModlogProps {
104 userId?: number | null;
105 modId?: number | null;
106 actionType: ModlogActionType;
109 function getActionFromString(action?: string): ModlogActionType {
110 return action !== undefined ? (action as ModlogActionType) : "All";
113 const getModlogActionMapper =
115 actionType: ModlogActionType,
116 getAction: (view: View) => { id: number; when_: string }
118 (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
119 const { id, when_ } = getAction(view);
126 moderator: view.moderator ?? view.admin,
130 function buildCombined({
138 admin_purged_comments,
139 admin_purged_communities,
140 admin_purged_persons,
143 banned_from_community,
144 transferred_to_community,
145 }: GetModlogResponse): ModlogType[] {
146 const combined = removed_posts
148 getModlogActionMapper(
150 ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
155 getModlogActionMapper(
157 ({ mod_lock_post }: ModLockPostView) => mod_lock_post
163 getModlogActionMapper(
165 ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
170 removed_comments.map(
171 getModlogActionMapper(
173 ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
178 removed_communities.map(
179 getModlogActionMapper(
180 "ModRemoveCommunity",
181 ({ mod_remove_community }: ModRemoveCommunityView) =>
187 banned_from_community.map(
188 getModlogActionMapper(
189 "ModBanFromCommunity",
190 ({ mod_ban_from_community }: ModBanFromCommunityView) =>
191 mod_ban_from_community
196 added_to_community.map(
197 getModlogActionMapper(
199 ({ mod_add_community }: ModAddCommunityView) => mod_add_community
204 transferred_to_community.map(
205 getModlogActionMapper(
206 "ModTransferCommunity",
207 ({ mod_transfer_community }: ModTransferCommunityView) =>
208 mod_transfer_community
214 getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add)
219 getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban)
223 admin_purged_persons.map(
224 getModlogActionMapper(
226 ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
231 admin_purged_communities.map(
232 getModlogActionMapper(
233 "AdminPurgeCommunity",
234 ({ admin_purge_community }: AdminPurgeCommunityView) =>
235 admin_purge_community
240 admin_purged_posts.map(
241 getModlogActionMapper(
243 ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
248 admin_purged_comments.map(
249 getModlogActionMapper(
251 ({ admin_purge_comment }: AdminPurgeCommentView) =>
258 combined.sort((a, b) => b.when_.localeCompare(a.when_));
263 function renderModlogType({ type_, view }: ModlogType) {
265 case "ModRemovePost": {
266 const mrpv = view as ModRemovePostView;
268 mod_remove_post: { reason, removed },
274 <span>{removed ? "Removed " : "Restored "}</span>
276 Post <Link to={`/post/${id}`}>{name}</Link>
280 <div>reason: {reason}</div>
287 case "ModLockPost": {
289 mod_lock_post: { locked },
291 } = view as ModLockPostView;
295 <span>{locked ? "Locked " : "Unlocked "}</span>
297 Post <Link to={`/post/${id}`}>{name}</Link>
303 case "ModFeaturePost": {
305 mod_feature_post: { featured, is_featured_community },
307 } = view as ModFeaturePostView;
311 <span>{featured ? "Featured " : "Unfeatured "}</span>
313 Post <Link to={`/post/${id}`}>{name}</Link>
315 <span>{is_featured_community ? " In Community" : " In Local"}</span>
319 case "ModRemoveComment": {
320 const mrc = view as ModRemoveCommentView;
322 mod_remove_comment: { reason, removed },
323 comment: { id, content },
329 <span>{removed ? "Removed " : "Restored "}</span>
331 Comment <Link to={`/comment/${id}`}>{content}</Link>
335 by <PersonListing person={commenter} />
339 <div>reason: {reason}</div>
346 case "ModRemoveCommunity": {
347 const mrco = view as ModRemoveCommunityView;
349 mod_remove_community: { reason, expires, removed },
355 <span>{removed ? "Removed " : "Restored "}</span>
357 Community <CommunityLink community={community} />
361 <div>reason: {reason}</div>
366 <div>expires: {moment.utc(expires).fromNow()}</div>
373 case "ModBanFromCommunity": {
374 const mbfc = view as ModBanFromCommunityView;
376 mod_ban_from_community: { reason, expires, banned },
383 <span>{banned ? "Banned " : "Unbanned "}</span>
385 <PersonListing person={banned_person} />
387 <span> from the community </span>
389 <CommunityLink community={community} />
393 <div>reason: {reason}</div>
398 <div>expires: {moment.utc(expires).fromNow()}</div>
405 case "ModAddCommunity": {
407 mod_add_community: { removed },
410 } = view as ModAddCommunityView;
414 <span>{removed ? "Removed " : "Appointed "}</span>
416 <PersonListing person={modded_person} />
418 <span> as a mod to the community </span>
420 <CommunityLink community={community} />
426 case "ModTransferCommunity": {
427 const { community, modded_person } = view as ModTransferCommunityView;
431 <span>Transferred</span>
433 <CommunityLink community={community} />
437 <PersonListing person={modded_person} />
445 mod_ban: { reason, expires, banned },
447 } = view as ModBanView;
451 <span>{banned ? "Banned " : "Unbanned "}</span>
453 <PersonListing person={banned_person} />
457 <div>reason: {reason}</div>
462 <div>expires: {moment.utc(expires).fromNow()}</div>
471 mod_add: { removed },
473 } = view as ModAddView;
477 <span>{removed ? "Removed " : "Appointed "}</span>
479 <PersonListing person={modded_person} />
481 <span> as an admin </span>
485 case "AdminPurgePerson": {
487 admin_purge_person: { reason },
488 } = view as AdminPurgePersonView;
492 <span>Purged a Person</span>
495 <div>reason: {reason}</div>
502 case "AdminPurgeCommunity": {
504 admin_purge_community: { reason },
505 } = view as AdminPurgeCommunityView;
509 <span>Purged a Community</span>
512 <div>reason: {reason}</div>
519 case "AdminPurgePost": {
521 admin_purge_post: { reason },
523 } = view as AdminPurgePostView;
527 <span>Purged a Post from from </span>
528 <CommunityLink community={community} />
531 <div>reason: {reason}</div>
538 case "AdminPurgeComment": {
540 admin_purge_comment: { reason },
542 } = view as AdminPurgeCommentView;
547 Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
551 <div>reason: {reason}</div>
571 filterType: FilterType;
572 onChange: (option: Choice) => void;
573 value?: number | null;
574 onSearch: (text: string) => void;
578 <div className="col-sm-6 form-group">
579 <label className="col-form-label" htmlFor={`filter-${filterType}`}>
580 {i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
583 id={`filter-${filterType}`}
587 label: i18n.t("all"),
598 async function createNewOptions({
604 oldOptions: Choice[];
607 const newOptions: Choice[] = [];
610 const selectedUser = oldOptions.find(
611 ({ value }) => value === id.toString()
615 newOptions.push(selectedUser);
619 if (text.length > 0) {
621 ...(await fetchUsers(text))
622 .slice(0, Number(fetchLimit))
623 .map<Choice>(personToChoice)
630 export class Modlog extends Component<
631 RouteComponentProps<{ communityId?: string }>,
634 private isoData = setIsoData(this.context);
636 state: ModlogState = {
637 res: { state: "empty" },
638 communityRes: { state: "empty" },
639 loadingModSearch: false,
640 loadingUserSearch: false,
641 userSearchOptions: [],
642 modSearchOptions: [],
646 props: RouteComponentProps<{ communityId?: string }>,
649 super(props, context);
650 this.handlePageChange = this.handlePageChange.bind(this);
651 this.handleUserChange = this.handleUserChange.bind(this);
652 this.handleModChange = this.handleModChange.bind(this);
654 // Only fetch the data if coming from another route
655 if (FirstLoadService.isFirstLoad) {
656 const [res, communityRes, filteredModRes, filteredUserRes] =
657 this.isoData.routeData;
664 if (filteredModRes.state === "success") {
667 modSearchOptions: [personToChoice(filteredModRes.data.person_view)],
671 if (filteredUserRes.state === "success") {
674 userSearchOptions: [personToChoice(filteredUserRes.data.person_view)],
681 const res = this.state.res;
682 const combined = res.state == "success" ? buildCombined(res.data) : [];
689 <MomentTime published={i.when_} />
692 {this.amAdminOrMod && i.moderator ? (
693 <PersonListing person={i.moderator} />
695 <div>{this.modOrAdminText(i.moderator)}</div>
698 <td>{renderModlogType(i)}</td>
705 get amAdminOrMod(): boolean {
707 this.state.communityRes.state == "success" &&
708 amMod(this.state.communityRes.data.moderators);
709 return amAdmin() || amMod_;
712 modOrAdminText(person?: Person): string {
714 this.isoData.site_res.admins.some(
715 ({ person: { id } }) => id === person.id
721 get documentTitle(): string {
722 return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
732 const { actionType, modId, userId } = getModlogQueryParams();
735 <div className="container-lg">
737 title={this.documentTitle}
738 path={this.context.router.route.match.url}
743 className="alert alert-warning text-sm-start text-xs-center"
747 icon="alert-triangle"
749 classes="mr-sm-2 mx-auto d-sm-inline d-block"
751 <T i18nKey="modlog_content_warning" class="d-inline">
755 {this.state.communityRes.state === "success" && (
758 className="text-body"
759 to={`/c/${this.state.communityRes.data.community_view.community.name}`}
761 /c/{this.state.communityRes.data.community_view.community.name}{" "}
763 <span>{i18n.t("modlog")}</span>
766 <div className="form-row">
769 onChange={linkEvent(this, this.handleFilterActionChange)}
770 className="custom-select col-sm-6"
773 <option disabled aria-hidden="true">
774 {i18n.t("filter_by_action")}
776 <option value={"All"}>{i18n.t("all")}</option>
777 <option value={"ModRemovePost"}>Removing Posts</option>
778 <option value={"ModLockPost"}>Locking Posts</option>
779 <option value={"ModFeaturePost"}>Featuring Posts</option>
780 <option value={"ModRemoveComment"}>Removing Comments</option>
781 <option value={"ModRemoveCommunity"}>Removing Communities</option>
782 <option value={"ModBanFromCommunity"}>
783 Banning From Communities
785 <option value={"ModAddCommunity"}>Adding Mod to Community</option>
786 <option value={"ModTransferCommunity"}>
787 Transferring Communities
789 <option value={"ModAdd"}>Adding Mod to Site</option>
790 <option value={"ModBan"}>Banning From Site</option>
793 <div className="form-row mb-2">
796 onChange={this.handleUserChange}
797 onSearch={this.handleSearchUsers}
799 options={userSearchOptions}
800 loading={loadingUserSearch}
802 {!this.isoData.site_res.site_view.local_site
803 .hide_modlog_mod_names && (
806 onChange={this.handleModChange}
807 onSearch={this.handleSearchMods}
809 options={modSearchOptions}
810 loading={loadingModSearch}
814 {this.renderModlogTable()}
820 renderModlogTable() {
821 switch (this.state.res.state) {
829 const page = getModlogQueryParams().page;
831 <div className="table-responsive">
832 <table id="modlog_table" className="table table-sm table-hover">
833 <thead className="pointer">
835 <th> {i18n.t("time")}</th>
836 <th>{i18n.t("mod")}</th>
837 <th>{i18n.t("action")}</th>
842 <Paginator page={page} onChange={this.handlePageChange} />
849 handleFilterActionChange(i: Modlog, event: any) {
851 actionType: event.target.value as ModlogActionType,
856 handlePageChange(page: number) {
857 this.updateUrl({ page });
860 handleUserChange(option: Choice) {
861 this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
864 handleModChange(option: Choice) {
865 this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
868 handleSearchUsers = debounce(async (text: string) => {
869 const { userId } = getModlogQueryParams();
870 const { userSearchOptions } = this.state;
871 this.setState({ loadingUserSearch: true });
873 const newOptions = await createNewOptions({
876 oldOptions: userSearchOptions,
880 userSearchOptions: newOptions,
881 loadingUserSearch: false,
885 handleSearchMods = debounce(async (text: string) => {
886 const { modId } = getModlogQueryParams();
887 const { modSearchOptions } = this.state;
888 this.setState({ loadingModSearch: true });
890 const newOptions = await createNewOptions({
893 oldOptions: modSearchOptions,
897 modSearchOptions: newOptions,
898 loadingModSearch: false,
902 async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
905 actionType: urlActionType,
908 } = getModlogQueryParams();
910 const queryParams: QueryParams<ModlogProps> = {
911 page: (page ?? urlPage).toString(),
912 actionType: actionType ?? urlActionType,
913 modId: getUpdatedSearchId(modId, urlModId),
914 userId: getUpdatedSearchId(userId, urlUserId),
917 const communityId = this.props.match.params.communityId;
919 this.props.history.push(
920 `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
925 await this.refetch();
929 const auth = myAuth();
930 const { actionType, page, modId, userId } = getModlogQueryParams();
931 const { communityId: urlCommunityId } = this.props.match.params;
932 const communityId = getIdFromString(urlCommunityId);
934 this.setState({ res: { state: "loading" } });
936 res: await HttpService.client.getModlog({
937 community_id: communityId,
941 other_person_id: userId ?? undefined,
942 mod_person_id: !this.isoData.site_res.site_view.local_site
943 .hide_modlog_mod_names
951 this.setState({ communityRes: { state: "loading" } });
953 communityRes: await HttpService.client.getCommunity({
961 static fetchInitialData({
964 query: { modId: urlModId, page, userId: urlUserId, actionType },
967 }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<
970 const pathSplit = path.split("/");
971 const promises: Promise<RequestState<any>>[] = [];
972 const communityId = getIdFromString(pathSplit[2]);
973 const modId = !site.site_view.local_site.hide_modlog_mod_names
974 ? getIdFromString(urlModId)
976 const userId = getIdFromString(urlUserId);
978 const modlogForm: GetModlog = {
979 page: getPageFromString(page),
981 community_id: communityId,
982 type_: getActionFromString(actionType),
983 mod_person_id: modId,
984 other_person_id: userId,
988 promises.push(client.getModlog(modlogForm));
991 const communityForm: GetCommunity = {
995 promises.push(client.getCommunity(communityForm));
997 promises.push(Promise.resolve({ state: "empty" }));
1001 const getPersonForm: GetPersonDetails = {
1006 promises.push(client.getPersonDetails(getPersonForm));
1008 promises.push(Promise.resolve({ state: "empty" }));
1012 const getPersonForm: GetPersonDetails = {
1017 promises.push(client.getPersonDetails(getPersonForm));
1019 promises.push(Promise.resolve({ state: "empty" }));