15 } from "@utils/helpers";
16 import { amAdmin, amMod } from "@utils/roles";
17 import type { QueryParams } from "@utils/types";
18 import { Choice, RouteDataResponse } from "@utils/types";
19 import { NoOptionI18nKeys } from "i18next";
20 import { Component, linkEvent } from "inferno";
21 import { T } from "inferno-i18next-dess";
22 import { Link } from "inferno-router";
23 import { RouteComponentProps } from "inferno-router/dist/Route";
25 AdminPurgeCommentView,
26 AdminPurgeCommunityView,
34 GetPersonDetailsResponse,
37 ModBanFromCommunityView,
42 ModRemoveCommunityView,
44 ModTransferCommunityView,
47 } from "lemmy-js-client";
48 import { fetchLimit } from "../config";
49 import { InitialFetchRequest } from "../interfaces";
50 import { FirstLoadService, I18NextService } from "../services";
51 import { HttpService, RequestState } from "../services/HttpService";
52 import { HtmlTags } from "./common/html-tags";
53 import { Icon, Spinner } from "./common/icon";
54 import { MomentTime } from "./common/moment-time";
55 import { Paginator } from "./common/paginator";
56 import { SearchableSelect } from "./common/searchable-select";
57 import { CommunityLink } from "./community/community-link";
58 import { PersonListing } from "./person/person-listing";
60 type FilterType = "mod" | "user";
66 | ModRemoveCommentView
67 | ModRemoveCommunityView
68 | ModBanFromCommunityView
71 | ModTransferCommunityView
73 | AdminPurgePersonView
74 | AdminPurgeCommunityView
76 | AdminPurgeCommentView;
78 type ModlogData = RouteDataResponse<{
79 res: GetModlogResponse;
80 communityRes: GetCommunityResponse;
81 modUserResponse: GetPersonDetailsResponse;
82 userResponse: GetPersonDetailsResponse;
85 interface ModlogType {
87 type_: ModlogActionType;
93 const getModlogQueryParams = () =>
94 getQueryParams<ModlogProps>({
95 actionType: getActionFromString,
96 modId: getIdFromString,
97 userId: getIdFromString,
98 page: getPageFromString,
101 interface ModlogState {
102 res: RequestState<GetModlogResponse>;
103 communityRes: RequestState<GetCommunityResponse>;
104 loadingModSearch: boolean;
105 loadingUserSearch: boolean;
106 modSearchOptions: Choice[];
107 userSearchOptions: Choice[];
110 interface ModlogProps {
112 userId?: number | null;
113 modId?: number | null;
114 actionType: ModlogActionType;
117 function getActionFromString(action?: string): ModlogActionType {
118 return action !== undefined ? (action as ModlogActionType) : "All";
121 const getModlogActionMapper =
123 actionType: ModlogActionType,
124 getAction: (view: View) => { id: number; when_: string },
126 (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
127 const { id, when_ } = getAction(view);
134 moderator: view.moderator ?? view.admin,
138 function buildCombined({
146 admin_purged_comments,
147 admin_purged_communities,
148 admin_purged_persons,
151 banned_from_community,
152 transferred_to_community,
153 }: GetModlogResponse): ModlogType[] {
154 const combined = removed_posts
156 getModlogActionMapper(
158 ({ mod_remove_post }: ModRemovePostView) => mod_remove_post,
163 getModlogActionMapper(
165 ({ mod_lock_post }: ModLockPostView) => mod_lock_post,
171 getModlogActionMapper(
173 ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post,
178 removed_comments.map(
179 getModlogActionMapper(
181 ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment,
186 removed_communities.map(
187 getModlogActionMapper(
188 "ModRemoveCommunity",
189 ({ mod_remove_community }: ModRemoveCommunityView) =>
190 mod_remove_community,
195 banned_from_community.map(
196 getModlogActionMapper(
197 "ModBanFromCommunity",
198 ({ mod_ban_from_community }: ModBanFromCommunityView) =>
199 mod_ban_from_community,
204 added_to_community.map(
205 getModlogActionMapper(
207 ({ mod_add_community }: ModAddCommunityView) => mod_add_community,
212 transferred_to_community.map(
213 getModlogActionMapper(
214 "ModTransferCommunity",
215 ({ mod_transfer_community }: ModTransferCommunityView) =>
216 mod_transfer_community,
222 getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add),
227 getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban),
231 admin_purged_persons.map(
232 getModlogActionMapper(
234 ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person,
239 admin_purged_communities.map(
240 getModlogActionMapper(
241 "AdminPurgeCommunity",
242 ({ admin_purge_community }: AdminPurgeCommunityView) =>
243 admin_purge_community,
248 admin_purged_posts.map(
249 getModlogActionMapper(
251 ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post,
256 admin_purged_comments.map(
257 getModlogActionMapper(
259 ({ admin_purge_comment }: AdminPurgeCommentView) =>
266 combined.sort((a, b) => b.when_.localeCompare(a.when_));
271 function renderModlogType({ type_, view }: ModlogType) {
273 case "ModRemovePost": {
274 const mrpv = view as ModRemovePostView;
276 mod_remove_post: { reason, removed },
282 <span>{removed ? "Removed " : "Restored "}</span>
284 Post <Link to={`/post/${id}`}>{name}</Link>
288 <div>reason: {reason}</div>
295 case "ModLockPost": {
297 mod_lock_post: { locked },
299 } = view as ModLockPostView;
303 <span>{locked ? "Locked " : "Unlocked "}</span>
305 Post <Link to={`/post/${id}`}>{name}</Link>
311 case "ModFeaturePost": {
313 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>
325 {is_featured_community
327 : " in Local, from community "}
329 <CommunityLink community={community} />
333 case "ModRemoveComment": {
334 const mrc = view as ModRemoveCommentView;
336 mod_remove_comment: { reason, removed },
337 comment: { id, content },
343 <span>{removed ? "Removed " : "Restored "}</span>
345 Comment <Link to={`/comment/${id}`}>{content}</Link>
349 by <PersonListing person={commenter} />
353 <div>reason: {reason}</div>
360 case "ModRemoveCommunity": {
361 const mrco = view as ModRemoveCommunityView;
363 mod_remove_community: { reason, expires, removed },
369 <span>{removed ? "Removed " : "Restored "}</span>
371 Community <CommunityLink community={community} />
375 <div>reason: {reason}</div>
380 <div>expires: {formatPastDate(expires)}</div>
387 case "ModBanFromCommunity": {
388 const mbfc = view as ModBanFromCommunityView;
390 mod_ban_from_community: { reason, expires, banned },
397 <span>{banned ? "Banned " : "Unbanned "}</span>
399 <PersonListing person={banned_person} />
401 <span> from the community </span>
403 <CommunityLink community={community} />
407 <div>reason: {reason}</div>
412 <div>expires: {formatPastDate(expires)}</div>
419 case "ModAddCommunity": {
421 mod_add_community: { removed },
424 } = view as ModAddCommunityView;
428 <span>{removed ? "Removed " : "Appointed "}</span>
430 <PersonListing person={modded_person} />
432 <span> as a mod to the community </span>
434 <CommunityLink community={community} />
440 case "ModTransferCommunity": {
441 const { community, modded_person } = view as ModTransferCommunityView;
445 <span>Transferred</span>
447 <CommunityLink community={community} />
451 <PersonListing person={modded_person} />
459 mod_ban: { reason, expires, banned },
461 } = view as ModBanView;
465 <span>{banned ? "Banned " : "Unbanned "}</span>
467 <PersonListing person={banned_person} />
471 <div>reason: {reason}</div>
476 <div>expires: {formatPastDate(expires)}</div>
485 mod_add: { removed },
487 } = view as ModAddView;
491 <span>{removed ? "Removed " : "Appointed "}</span>
493 <PersonListing person={modded_person} />
495 <span> as an admin </span>
499 case "AdminPurgePerson": {
501 admin_purge_person: { reason },
502 } = view as AdminPurgePersonView;
506 <span>Purged a Person</span>
509 <div>reason: {reason}</div>
516 case "AdminPurgeCommunity": {
518 admin_purge_community: { reason },
519 } = view as AdminPurgeCommunityView;
523 <span>Purged a Community</span>
526 <div>reason: {reason}</div>
533 case "AdminPurgePost": {
535 admin_purge_post: { reason },
537 } = view as AdminPurgePostView;
541 <span>Purged a Post from </span>
542 <CommunityLink community={community} />
545 <div>reason: {reason}</div>
552 case "AdminPurgeComment": {
554 admin_purge_comment: { reason },
556 } = view as AdminPurgeCommentView;
561 Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
565 <div>reason: {reason}</div>
585 filterType: FilterType;
586 onChange: (option: Choice) => void;
587 value?: number | null;
588 onSearch: (text: string) => void;
592 <div className="col-sm-6 mb-3">
593 <label className="mb-2" htmlFor={`filter-${filterType}`}>
594 {I18NextService.i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
597 id={`filter-${filterType}`}
601 label: I18NextService.i18n.t("all"),
612 async function createNewOptions({
618 oldOptions: Choice[];
621 const newOptions: Choice[] = [];
624 const selectedUser = oldOptions.find(
625 ({ value }) => value === id.toString(),
629 newOptions.push(selectedUser);
633 if (text.length > 0) {
635 ...(await fetchUsers(text))
636 .slice(0, Number(fetchLimit))
637 .map<Choice>(personToChoice),
644 export class Modlog extends Component<
645 RouteComponentProps<{ communityId?: string }>,
648 private isoData = setIsoData<ModlogData>(this.context);
650 state: ModlogState = {
651 res: { state: "empty" },
652 communityRes: { state: "empty" },
653 loadingModSearch: false,
654 loadingUserSearch: false,
655 userSearchOptions: [],
656 modSearchOptions: [],
660 props: RouteComponentProps<{ communityId?: string }>,
663 super(props, context);
664 this.handlePageChange = this.handlePageChange.bind(this);
665 this.handleUserChange = this.handleUserChange.bind(this);
666 this.handleModChange = this.handleModChange.bind(this);
668 // Only fetch the data if coming from another route
669 if (FirstLoadService.isFirstLoad) {
670 const { res, communityRes, modUserResponse, userResponse } =
671 this.isoData.routeData;
679 if (modUserResponse.state === "success") {
682 modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
686 if (userResponse.state === "success") {
689 userSearchOptions: [personToChoice(userResponse.data.person_view)],
695 async componentDidMount() {
696 await this.refetch();
700 const res = this.state.res;
701 const combined = res.state === "success" ? buildCombined(res.data) : [];
708 <MomentTime published={i.when_} />
711 {this.amAdminOrMod && i.moderator ? (
712 <PersonListing person={i.moderator} />
714 <div>{this.modOrAdminText(i.moderator)}</div>
717 <td>{renderModlogType(i)}</td>
724 get amAdminOrMod(): boolean {
726 this.state.communityRes.state === "success" &&
727 amMod(this.state.communityRes.data.moderators);
728 return amAdmin() || amMod_;
731 modOrAdminText(person?: Person): string {
733 this.isoData.site_res.admins.some(
734 ({ person: { id } }) => id === person.id,
736 ? I18NextService.i18n.t("admin")
737 : I18NextService.i18n.t("mod");
740 get documentTitle(): string {
741 return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
751 const { actionType, modId, userId } = getModlogQueryParams();
754 <div className="modlog container-lg">
756 title={this.documentTitle}
757 path={this.context.router.route.match.url}
760 <h1 className="h4 mb-4">{I18NextService.i18n.t("modlog")}</h1>
763 className="alert alert-warning text-sm-start text-xs-center"
767 icon="alert-triangle"
769 classes="me-sm-2 mx-auto d-sm-inline d-block"
771 <T i18nKey="modlog_content_warning" class="d-inline">
775 {this.state.communityRes.state === "success" && (
778 className="text-body"
779 to={`/c/${this.state.communityRes.data.community_view.community.name}`}
781 /c/{this.state.communityRes.data.community_view.community.name}{" "}
783 <span>{I18NextService.i18n.t("modlog")}</span>
786 <div className="row mb-2">
787 <div className="col-sm-6">
790 onChange={linkEvent(this, this.handleFilterActionChange)}
791 className="form-select"
794 <option disabled aria-hidden="true">
795 {I18NextService.i18n.t("filter_by_action")}
797 <option value={"All"}>{I18NextService.i18n.t("all")}</option>
798 <option value={"ModRemovePost"}>Removing Posts</option>
799 <option value={"ModLockPost"}>Locking Posts</option>
800 <option value={"ModFeaturePost"}>Featuring Posts</option>
801 <option value={"ModRemoveComment"}>Removing Comments</option>
802 <option value={"ModRemoveCommunity"}>Removing Communities</option>
803 <option value={"ModBanFromCommunity"}>
804 Banning From Communities
806 <option value={"ModAddCommunity"}>Adding Mod to Community</option>
807 <option value={"ModTransferCommunity"}>
808 Transferring Communities
810 <option value={"ModAdd"}>Adding Mod to Site</option>
811 <option value={"ModBan"}>Banning From Site</option>
815 <div className="row mb-2">
818 onChange={this.handleUserChange}
819 onSearch={this.handleSearchUsers}
821 options={userSearchOptions}
822 loading={loadingUserSearch}
824 {!this.isoData.site_res.site_view.local_site
825 .hide_modlog_mod_names && (
828 onChange={this.handleModChange}
829 onSearch={this.handleSearchMods}
831 options={modSearchOptions}
832 loading={loadingModSearch}
836 {this.renderModlogTable()}
841 renderModlogTable() {
842 switch (this.state.res.state) {
850 const page = getModlogQueryParams().page;
852 <div className="table-responsive">
853 <table id="modlog_table" className="table table-sm table-hover">
854 <thead className="pointer">
856 <th> {I18NextService.i18n.t("time")}</th>
857 <th>{I18NextService.i18n.t("mod")}</th>
858 <th>{I18NextService.i18n.t("action")}</th>
863 <Paginator page={page} onChange={this.handlePageChange} />
870 handleFilterActionChange(i: Modlog, event: any) {
872 actionType: event.target.value as ModlogActionType,
877 handlePageChange(page: number) {
878 this.updateUrl({ page });
881 handleUserChange(option: Choice) {
882 this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
885 handleModChange(option: Choice) {
886 this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
889 handleSearchUsers = debounce(async (text: string) => {
890 const { userId } = getModlogQueryParams();
891 const { userSearchOptions } = this.state;
892 this.setState({ loadingUserSearch: true });
894 const newOptions = await createNewOptions({
897 oldOptions: userSearchOptions,
901 userSearchOptions: newOptions,
902 loadingUserSearch: false,
906 handleSearchMods = debounce(async (text: string) => {
907 const { modId } = getModlogQueryParams();
908 const { modSearchOptions } = this.state;
909 this.setState({ loadingModSearch: true });
911 const newOptions = await createNewOptions({
914 oldOptions: modSearchOptions,
918 modSearchOptions: newOptions,
919 loadingModSearch: false,
923 async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
926 actionType: urlActionType,
929 } = getModlogQueryParams();
931 const queryParams: QueryParams<ModlogProps> = {
932 page: (page ?? urlPage).toString(),
933 actionType: actionType ?? urlActionType,
934 modId: getUpdatedSearchId(modId, urlModId),
935 userId: getUpdatedSearchId(userId, urlUserId),
938 const communityId = this.props.match.params.communityId;
940 this.props.history.push(
941 `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
946 await this.refetch();
950 const auth = myAuth();
951 const { actionType, page, modId, userId } = getModlogQueryParams();
952 const { communityId: urlCommunityId } = this.props.match.params;
953 const communityId = getIdFromString(urlCommunityId);
955 this.setState({ res: { state: "loading" } });
957 res: await HttpService.client.getModlog({
958 community_id: communityId,
962 other_person_id: userId ?? undefined,
963 mod_person_id: !this.isoData.site_res.site_view.local_site
964 .hide_modlog_mod_names
972 this.setState({ communityRes: { state: "loading" } });
974 communityRes: await HttpService.client.getCommunity({
982 static async fetchInitialData({
985 query: { modId: urlModId, page, userId: urlUserId, actionType },
988 }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
989 const pathSplit = path.split("/");
990 const communityId = getIdFromString(pathSplit[2]);
991 const modId = !site.site_view.local_site.hide_modlog_mod_names
992 ? getIdFromString(urlModId)
994 const userId = getIdFromString(urlUserId);
996 const modlogForm: GetModlog = {
997 page: getPageFromString(page),
999 community_id: communityId,
1000 type_: getActionFromString(actionType),
1001 mod_person_id: modId,
1002 other_person_id: userId,
1006 let communityResponse: RequestState<GetCommunityResponse> = {
1011 const communityForm: GetCommunity = {
1016 communityResponse = await client.getCommunity(communityForm);
1019 let modUserResponse: RequestState<GetPersonDetailsResponse> = {
1024 const getPersonForm: GetPersonDetails = {
1029 modUserResponse = await client.getPersonDetails(getPersonForm);
1032 let userResponse: RequestState<GetPersonDetailsResponse> = {
1037 const getPersonForm: GetPersonDetails = {
1042 userResponse = await client.getPersonDetails(getPersonForm);
1046 res: await client.getModlog(modlogForm),
1047 communityRes: communityResponse,