1 import { NoOptionI18nKeys } from "i18next";
2 import { Component, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
4 import { RouteComponentProps } from "inferno-router/dist/Route";
7 AdminPurgeCommunityView,
10 CommunityModeratorView,
16 GetPersonDetailsResponse,
19 ModBanFromCommunityView,
25 ModRemoveCommunityView,
27 ModTransferCommunityView,
32 } from "lemmy-js-client";
33 import moment from "moment";
34 import { Subscription } from "rxjs";
35 import { i18n } from "../i18next";
36 import { InitialFetchRequest } from "../interfaces";
37 import { WebSocketService } from "../services";
59 import { HtmlTags } from "./common/html-tags";
60 import { Spinner } from "./common/icon";
61 import { MomentTime } from "./common/moment-time";
62 import { Paginator } from "./common/paginator";
63 import { SearchableSelect } from "./common/searchable-select";
64 import { CommunityLink } from "./community/community-link";
65 import { PersonListing } from "./person/person-listing";
67 type FilterType = "mod" | "user";
73 | ModRemoveCommentView
74 | ModRemoveCommunityView
75 | ModBanFromCommunityView
78 | ModTransferCommunityView
80 | AdminPurgePersonView
81 | AdminPurgeCommunityView
83 | AdminPurgeCommentView;
85 interface ModlogType {
87 type_: ModlogActionType;
88 moderator?: PersonSafe;
93 const getModlogQueryParams = () =>
94 getQueryParams<ModlogProps>({
95 actionType: getActionFromString,
96 modId: getIdFromString,
97 userId: getIdFromString,
98 page: getPageFromString,
101 interface ModlogState {
102 res?: GetModlogResponse;
103 communityMods?: CommunityModeratorView[];
104 communityName?: string;
105 loadingModlog: boolean;
106 loadingModSearch: boolean;
107 loadingUserSearch: boolean;
108 modSearchOptions: Choice[];
109 userSearchOptions: Choice[];
112 interface ModlogProps {
114 userId?: number | null;
115 modId?: number | null;
116 actionType: ModlogActionType;
119 const getActionFromString = (action?: string) =>
121 ? ModlogActionType[action] ?? ModlogActionType.All
122 : ModlogActionType.All;
124 const getModlogActionMapper =
126 actionType: ModlogActionType,
127 getAction: (view: View) => { id: number; when_: string }
129 (view: View & { moderator?: PersonSafe; admin?: PersonSafe }): ModlogType => {
130 const { id, when_ } = getAction(view);
137 moderator: view.moderator ?? view.admin,
141 function buildCombined({
149 admin_purged_comments,
150 admin_purged_communities,
151 admin_purged_persons,
154 banned_from_community,
155 transferred_to_community,
156 }: GetModlogResponse): ModlogType[] {
157 const combined = removed_posts
159 getModlogActionMapper(
160 ModlogActionType.ModRemovePost,
161 ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
166 getModlogActionMapper(
167 ModlogActionType.ModLockPost,
168 ({ mod_lock_post }: ModLockPostView) => mod_lock_post
174 getModlogActionMapper(
175 ModlogActionType.ModFeaturePost,
176 ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
181 removed_comments.map(
182 getModlogActionMapper(
183 ModlogActionType.ModRemoveComment,
184 ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
189 removed_communities.map(
190 getModlogActionMapper(
191 ModlogActionType.ModRemoveCommunity,
192 ({ mod_remove_community }: ModRemoveCommunityView) =>
198 banned_from_community.map(
199 getModlogActionMapper(
200 ModlogActionType.ModBanFromCommunity,
201 ({ mod_ban_from_community }: ModBanFromCommunityView) =>
202 mod_ban_from_community
207 added_to_community.map(
208 getModlogActionMapper(
209 ModlogActionType.ModAddCommunity,
210 ({ mod_add_community }: ModAddCommunityView) => mod_add_community
215 transferred_to_community.map(
216 getModlogActionMapper(
217 ModlogActionType.ModTransferCommunity,
218 ({ mod_transfer_community }: ModTransferCommunityView) =>
219 mod_transfer_community
225 getModlogActionMapper(
226 ModlogActionType.ModAdd,
227 ({ mod_add }: ModAddView) => mod_add
233 getModlogActionMapper(
234 ModlogActionType.ModBan,
235 ({ mod_ban }: ModBanView) => mod_ban
240 admin_purged_persons.map(
241 getModlogActionMapper(
242 ModlogActionType.AdminPurgePerson,
243 ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
248 admin_purged_communities.map(
249 getModlogActionMapper(
250 ModlogActionType.AdminPurgeCommunity,
251 ({ admin_purge_community }: AdminPurgeCommunityView) =>
252 admin_purge_community
257 admin_purged_posts.map(
258 getModlogActionMapper(
259 ModlogActionType.AdminPurgePost,
260 ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
265 admin_purged_comments.map(
266 getModlogActionMapper(
267 ModlogActionType.AdminPurgeComment,
268 ({ admin_purge_comment }: AdminPurgeCommentView) =>
275 combined.sort((a, b) => b.when_.localeCompare(a.when_));
280 function renderModlogType({ type_, view }: ModlogType) {
282 case ModlogActionType.ModRemovePost: {
283 const mrpv = view as ModRemovePostView;
285 mod_remove_post: { reason, removed },
291 <span>{removed ? "Removed " : "Restored "}</span>
293 Post <Link to={`/post/${id}`}>{name}</Link>
297 <div>reason: {reason}</div>
304 case ModlogActionType.ModLockPost: {
306 mod_lock_post: { locked },
308 } = view as ModLockPostView;
312 <span>{locked ? "Locked " : "Unlocked "}</span>
314 Post <Link to={`/post/${id}`}>{name}</Link>
320 case ModlogActionType.ModFeaturePost: {
322 mod_feature_post: { featured, is_featured_community },
324 } = view as ModFeaturePostView;
328 <span>{featured ? "Featured " : "Unfeatured "}</span>
330 Post <Link to={`/post/${id}`}>{name}</Link>
332 <span>{is_featured_community ? " In Community" : " In Local"}</span>
336 case ModlogActionType.ModRemoveComment: {
337 const mrc = view as ModRemoveCommentView;
339 mod_remove_comment: { reason, removed },
340 comment: { id, content },
346 <span>{removed ? "Removed " : "Restored "}</span>
348 Comment <Link to={`/comment/${id}`}>{content}</Link>
352 by <PersonListing person={commenter} />
356 <div>reason: {reason}</div>
363 case ModlogActionType.ModRemoveCommunity: {
364 const mrco = view as ModRemoveCommunityView;
366 mod_remove_community: { reason, expires, removed },
372 <span>{removed ? "Removed " : "Restored "}</span>
374 Community <CommunityLink community={community} />
378 <div>reason: {reason}</div>
383 <div>expires: {moment.utc(expires).fromNow()}</div>
390 case ModlogActionType.ModBanFromCommunity: {
391 const mbfc = view as ModBanFromCommunityView;
393 mod_ban_from_community: { reason, expires, banned },
400 <span>{banned ? "Banned " : "Unbanned "}</span>
402 <PersonListing person={banned_person} />
404 <span> from the community </span>
406 <CommunityLink community={community} />
410 <div>reason: {reason}</div>
415 <div>expires: {moment.utc(expires).fromNow()}</div>
422 case ModlogActionType.ModAddCommunity: {
424 mod_add_community: { removed },
427 } = view as ModAddCommunityView;
431 <span>{removed ? "Removed " : "Appointed "}</span>
433 <PersonListing person={modded_person} />
435 <span> as a mod to the community </span>
437 <CommunityLink community={community} />
443 case ModlogActionType.ModTransferCommunity: {
445 mod_transfer_community: { removed },
448 } = view as ModTransferCommunityView;
452 <span>{removed ? "Removed " : "Transferred "}</span>
454 <CommunityLink community={community} />
458 <PersonListing person={modded_person} />
464 case ModlogActionType.ModBan: {
466 mod_ban: { reason, expires, banned },
468 } = view as ModBanView;
472 <span>{banned ? "Banned " : "Unbanned "}</span>
474 <PersonListing person={banned_person} />
478 <div>reason: {reason}</div>
483 <div>expires: {moment.utc(expires).fromNow()}</div>
490 case ModlogActionType.ModAdd: {
492 mod_add: { removed },
494 } = view as ModAddView;
498 <span>{removed ? "Removed " : "Appointed "}</span>
500 <PersonListing person={modded_person} />
502 <span> as an admin </span>
506 case ModlogActionType.AdminPurgePerson: {
508 admin_purge_person: { reason },
509 } = view as AdminPurgePersonView;
513 <span>Purged a Person</span>
516 <div>reason: {reason}</div>
523 case ModlogActionType.AdminPurgeCommunity: {
525 admin_purge_community: { reason },
526 } = view as AdminPurgeCommunityView;
530 <span>Purged a Community</span>
533 <div>reason: {reason}</div>
540 case ModlogActionType.AdminPurgePost: {
542 admin_purge_post: { reason },
544 } = view as AdminPurgePostView;
548 <span>Purged a Post from from </span>
549 <CommunityLink community={community} />
552 <div>reason: {reason}</div>
559 case ModlogActionType.AdminPurgeComment: {
561 admin_purge_comment: { reason },
563 } = view as AdminPurgeCommentView;
568 Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
572 <div>reason: {reason}</div>
592 filterType: FilterType;
593 onChange: (option: Choice) => void;
594 value?: number | null;
595 onSearch: (text: string) => void;
599 <div className="col-sm-6 form-group">
600 <label className="col-form-label" htmlFor={`filter-${filterType}`}>
601 {i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
604 id={`filter-${filterType}`}
608 label: i18n.t("all"),
619 async function createNewOptions({
625 oldOptions: Choice[];
628 const newOptions: Choice[] = [];
631 const selectedUser = oldOptions.find(
632 ({ value }) => value === id.toString()
636 newOptions.push(selectedUser);
640 if (text.length > 0) {
642 ...(await fetchUsers(text)).users
643 .slice(0, fetchLimit)
644 .map<Choice>(personToChoice)
651 export class Modlog extends Component<
652 RouteComponentProps<{ communityId?: string }>,
655 private isoData = setIsoData(this.context);
656 private subscription?: Subscription;
658 state: ModlogState = {
660 loadingModSearch: false,
661 loadingUserSearch: false,
662 userSearchOptions: [],
663 modSearchOptions: [],
667 props: RouteComponentProps<{ communityId?: string }>,
670 super(props, context);
671 this.handlePageChange = this.handlePageChange.bind(this);
672 this.handleUserChange = this.handleUserChange.bind(this);
673 this.handleModChange = this.handleModChange.bind(this);
675 this.parseMessage = this.parseMessage.bind(this);
676 this.subscription = wsSubscribe(this.parseMessage);
678 // Only fetch the data if coming from another route
679 if (this.isoData.path === this.context.router.route.match.url) {
682 res: this.isoData.routeData[0] as GetModlogResponse,
685 const communityRes: GetCommunityResponse | undefined =
686 this.isoData.routeData[1];
688 // Getting the moderators
691 communityMods: communityRes?.moderators,
694 const filteredModRes: GetPersonDetailsResponse | undefined =
695 this.isoData.routeData[2];
696 if (filteredModRes) {
699 modSearchOptions: [personToChoice(filteredModRes.person_view)],
703 const filteredUserRes: GetPersonDetailsResponse | undefined =
704 this.isoData.routeData[3];
705 if (filteredUserRes) {
708 userSearchOptions: [personToChoice(filteredUserRes.person_view)],
712 this.state = { ...this.state, loadingModlog: false };
718 componentWillUnmount() {
720 this.subscription?.unsubscribe();
725 const res = this.state.res;
726 const combined = res ? buildCombined(res) : [];
733 <MomentTime published={i.when_} />
736 {this.amAdminOrMod && i.moderator ? (
737 <PersonListing person={i.moderator} />
739 <div>{this.modOrAdminText(i.moderator)}</div>
742 <td>{renderModlogType(i)}</td>
749 get amAdminOrMod(): boolean {
750 return amAdmin() || amMod(this.state.communityMods);
753 modOrAdminText(person?: PersonSafe): string {
755 this.isoData.site_res.admins.some(
756 ({ person: { id } }) => id === person.id
762 get documentTitle(): string {
763 return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
775 const { actionType, page, modId, userId } = getModlogQueryParams();
778 <div className="container-lg">
780 title={this.documentTitle}
781 path={this.context.router.route.match.url}
787 <Link className="text-body" to={`/c/${communityName}`}>
788 /c/{communityName}{" "}
791 <span>{i18n.t("modlog")}</span>
793 <div className="form-row">
796 onChange={linkEvent(this, this.handleFilterActionChange)}
797 className="custom-select col-sm-6"
800 <option disabled aria-hidden="true">
801 {i18n.t("filter_by_action")}
803 <option value={ModlogActionType.All}>{i18n.t("all")}</option>
804 <option value={ModlogActionType.ModRemovePost}>
807 <option value={ModlogActionType.ModLockPost}>
810 <option value={ModlogActionType.ModFeaturePost}>
813 <option value={ModlogActionType.ModRemoveComment}>
816 <option value={ModlogActionType.ModRemoveCommunity}>
819 <option value={ModlogActionType.ModBanFromCommunity}>
820 Banning From Communities
822 <option value={ModlogActionType.ModAddCommunity}>
823 Adding Mod to Community
825 <option value={ModlogActionType.ModTransferCommunity}>
826 Transferring Communities
828 <option value={ModlogActionType.ModAdd}>
831 <option value={ModlogActionType.ModBan}>Banning From Site</option>
834 <div className="form-row mb-2">
837 onChange={this.handleUserChange}
838 onSearch={this.handleSearchUsers}
840 options={userSearchOptions}
841 loading={loadingUserSearch}
843 {!this.isoData.site_res.site_view.local_site
844 .hide_modlog_mod_names && (
847 onChange={this.handleModChange}
848 onSearch={this.handleSearchMods}
850 options={modSearchOptions}
851 loading={loadingModSearch}
855 <div className="table-responsive">
861 <table id="modlog_table" className="table table-sm table-hover">
862 <thead className="pointer">
864 <th> {i18n.t("time")}</th>
865 <th>{i18n.t("mod")}</th>
866 <th>{i18n.t("action")}</th>
872 <Paginator page={page} onChange={this.handlePageChange} />
879 handleFilterActionChange(i: Modlog, event: any) {
881 actionType: ModlogActionType[event.target.value],
886 handlePageChange(page: number) {
887 this.updateUrl({ page });
890 handleUserChange(option: Choice) {
891 this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
894 handleModChange(option: Choice) {
895 this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
898 handleSearchUsers = debounce(async (text: string) => {
899 const { userId } = getModlogQueryParams();
900 const { userSearchOptions } = this.state;
901 this.setState({ loadingUserSearch: true });
903 const newOptions = await createNewOptions({
906 oldOptions: userSearchOptions,
910 userSearchOptions: newOptions,
911 loadingUserSearch: false,
915 handleSearchMods = debounce(async (text: string) => {
916 const { modId } = getModlogQueryParams();
917 const { modSearchOptions } = this.state;
918 this.setState({ loadingModSearch: true });
920 const newOptions = await createNewOptions({
923 oldOptions: modSearchOptions,
927 modSearchOptions: newOptions,
928 loadingModSearch: false,
932 updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
935 actionType: urlActionType,
938 } = getModlogQueryParams();
940 const queryParams: QueryParams<ModlogProps> = {
941 page: (page ?? urlPage).toString(),
942 actionType: actionType ?? urlActionType,
943 modId: getUpdatedSearchId(modId, urlModId),
944 userId: getUpdatedSearchId(userId, urlUserId),
947 const communityId = this.props.match.params.communityId;
949 this.props.history.push(
950 `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
964 const auth = myAuth(false);
965 const { actionType, page, modId, userId } = getModlogQueryParams();
966 const { communityId: urlCommunityId } = this.props.match.params;
967 const communityId = getIdFromString(urlCommunityId);
969 const modlogForm: GetModlog = {
970 community_id: communityId,
974 other_person_id: userId ?? undefined,
975 mod_person_id: !this.isoData.site_res.site_view.local_site
976 .hide_modlog_mod_names
982 WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
985 const communityForm: GetCommunity = {
990 WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
994 static fetchInitialData({
997 query: { modId: urlModId, page, userId: urlUserId, actionType },
1000 }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<any>[] {
1001 const pathSplit = path.split("/");
1002 const promises: Promise<any>[] = [];
1003 const communityId = getIdFromString(pathSplit[2]);
1004 const modId = !site.site_view.local_site.hide_modlog_mod_names
1005 ? getIdFromString(urlModId)
1007 const userId = getIdFromString(urlUserId);
1009 const modlogForm: GetModlog = {
1010 page: getPageFromString(page),
1012 community_id: communityId,
1013 type_: getActionFromString(actionType),
1014 mod_person_id: modId,
1015 other_person_id: userId,
1019 promises.push(client.getModlog(modlogForm));
1022 const communityForm: GetCommunity = {
1026 promises.push(client.getCommunity(communityForm));
1028 promises.push(Promise.resolve());
1032 const getPersonForm: GetPersonDetails = {
1037 promises.push(client.getPersonDetails(getPersonForm));
1039 promises.push(Promise.resolve());
1043 const getPersonForm: GetPersonDetails = {
1048 promises.push(client.getPersonDetails(getPersonForm));
1050 promises.push(Promise.resolve());
1056 parseMessage(msg: any) {
1057 const op = wsUserOp(msg);
1061 toast(i18n.t(msg.error), "danger");
1064 case UserOperation.GetModlog: {
1065 const res = wsJsonToRes<GetModlogResponse>(msg);
1066 window.scrollTo(0, 0);
1067 this.setState({ res, loadingModlog: false });
1072 case UserOperation.GetCommunity: {
1076 community: { name },
1078 } = wsJsonToRes<GetCommunityResponse>(msg);
1080 communityMods: moderators,
1081 communityName: name,