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,
11 CommunityModeratorView,
17 GetPersonDetailsResponse,
20 ModBanFromCommunityView,
26 ModRemoveCommunityView,
28 ModTransferCommunityView,
33 } from "lemmy-js-client";
34 import moment from "moment";
35 import { Subscription } from "rxjs";
36 import { i18n } from "../i18next";
37 import { InitialFetchRequest } from "../interfaces";
38 import { WebSocketService } from "../services";
60 import { HtmlTags } from "./common/html-tags";
61 import { Icon, Spinner } from "./common/icon";
62 import { MomentTime } from "./common/moment-time";
63 import { Paginator } from "./common/paginator";
64 import { SearchableSelect } from "./common/searchable-select";
65 import { CommunityLink } from "./community/community-link";
66 import { PersonListing } from "./person/person-listing";
68 type FilterType = "mod" | "user";
74 | ModRemoveCommentView
75 | ModRemoveCommunityView
76 | ModBanFromCommunityView
79 | ModTransferCommunityView
81 | AdminPurgePersonView
82 | AdminPurgeCommunityView
84 | AdminPurgeCommentView;
86 interface ModlogType {
88 type_: ModlogActionType;
89 moderator?: PersonSafe;
94 const getModlogQueryParams = () =>
95 getQueryParams<ModlogProps>({
96 actionType: getActionFromString,
97 modId: getIdFromString,
98 userId: getIdFromString,
99 page: getPageFromString,
102 interface ModlogState {
103 res?: GetModlogResponse;
104 communityMods?: CommunityModeratorView[];
105 communityName?: string;
106 loadingModlog: boolean;
107 loadingModSearch: boolean;
108 loadingUserSearch: boolean;
109 modSearchOptions: Choice[];
110 userSearchOptions: Choice[];
113 interface ModlogProps {
115 userId?: number | null;
116 modId?: number | null;
117 actionType: ModlogActionType;
120 const getActionFromString = (action?: string) =>
122 ? ModlogActionType[action] ?? ModlogActionType.All
123 : ModlogActionType.All;
125 const getModlogActionMapper =
127 actionType: ModlogActionType,
128 getAction: (view: View) => { id: number; when_: string }
130 (view: View & { moderator?: PersonSafe; admin?: PersonSafe }): ModlogType => {
131 const { id, when_ } = getAction(view);
138 moderator: view.moderator ?? view.admin,
142 function buildCombined({
150 admin_purged_comments,
151 admin_purged_communities,
152 admin_purged_persons,
155 banned_from_community,
156 transferred_to_community,
157 }: GetModlogResponse): ModlogType[] {
158 const combined = removed_posts
160 getModlogActionMapper(
161 ModlogActionType.ModRemovePost,
162 ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
167 getModlogActionMapper(
168 ModlogActionType.ModLockPost,
169 ({ mod_lock_post }: ModLockPostView) => mod_lock_post
175 getModlogActionMapper(
176 ModlogActionType.ModFeaturePost,
177 ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
182 removed_comments.map(
183 getModlogActionMapper(
184 ModlogActionType.ModRemoveComment,
185 ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
190 removed_communities.map(
191 getModlogActionMapper(
192 ModlogActionType.ModRemoveCommunity,
193 ({ mod_remove_community }: ModRemoveCommunityView) =>
199 banned_from_community.map(
200 getModlogActionMapper(
201 ModlogActionType.ModBanFromCommunity,
202 ({ mod_ban_from_community }: ModBanFromCommunityView) =>
203 mod_ban_from_community
208 added_to_community.map(
209 getModlogActionMapper(
210 ModlogActionType.ModAddCommunity,
211 ({ mod_add_community }: ModAddCommunityView) => mod_add_community
216 transferred_to_community.map(
217 getModlogActionMapper(
218 ModlogActionType.ModTransferCommunity,
219 ({ mod_transfer_community }: ModTransferCommunityView) =>
220 mod_transfer_community
226 getModlogActionMapper(
227 ModlogActionType.ModAdd,
228 ({ mod_add }: ModAddView) => mod_add
234 getModlogActionMapper(
235 ModlogActionType.ModBan,
236 ({ mod_ban }: ModBanView) => mod_ban
241 admin_purged_persons.map(
242 getModlogActionMapper(
243 ModlogActionType.AdminPurgePerson,
244 ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
249 admin_purged_communities.map(
250 getModlogActionMapper(
251 ModlogActionType.AdminPurgeCommunity,
252 ({ admin_purge_community }: AdminPurgeCommunityView) =>
253 admin_purge_community
258 admin_purged_posts.map(
259 getModlogActionMapper(
260 ModlogActionType.AdminPurgePost,
261 ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
266 admin_purged_comments.map(
267 getModlogActionMapper(
268 ModlogActionType.AdminPurgeComment,
269 ({ admin_purge_comment }: AdminPurgeCommentView) =>
276 combined.sort((a, b) => b.when_.localeCompare(a.when_));
281 function renderModlogType({ type_, view }: ModlogType) {
283 case ModlogActionType.ModRemovePost: {
284 const mrpv = view as ModRemovePostView;
286 mod_remove_post: { reason, removed },
292 <span>{removed ? "Removed " : "Restored "}</span>
294 Post <Link to={`/post/${id}`}>{name}</Link>
298 <div>reason: {reason}</div>
305 case ModlogActionType.ModLockPost: {
307 mod_lock_post: { locked },
309 } = view as ModLockPostView;
313 <span>{locked ? "Locked " : "Unlocked "}</span>
315 Post <Link to={`/post/${id}`}>{name}</Link>
321 case ModlogActionType.ModFeaturePost: {
323 mod_feature_post: { featured, is_featured_community },
325 } = view as ModFeaturePostView;
329 <span>{featured ? "Featured " : "Unfeatured "}</span>
331 Post <Link to={`/post/${id}`}>{name}</Link>
333 <span>{is_featured_community ? " In Community" : " In Local"}</span>
337 case ModlogActionType.ModRemoveComment: {
338 const mrc = view as ModRemoveCommentView;
340 mod_remove_comment: { reason, removed },
341 comment: { id, content },
347 <span>{removed ? "Removed " : "Restored "}</span>
349 Comment <Link to={`/comment/${id}`}>{content}</Link>
353 by <PersonListing person={commenter} />
357 <div>reason: {reason}</div>
364 case ModlogActionType.ModRemoveCommunity: {
365 const mrco = view as ModRemoveCommunityView;
367 mod_remove_community: { reason, expires, removed },
373 <span>{removed ? "Removed " : "Restored "}</span>
375 Community <CommunityLink community={community} />
379 <div>reason: {reason}</div>
384 <div>expires: {moment.utc(expires).fromNow()}</div>
391 case ModlogActionType.ModBanFromCommunity: {
392 const mbfc = view as ModBanFromCommunityView;
394 mod_ban_from_community: { reason, expires, banned },
401 <span>{banned ? "Banned " : "Unbanned "}</span>
403 <PersonListing person={banned_person} />
405 <span> from the community </span>
407 <CommunityLink community={community} />
411 <div>reason: {reason}</div>
416 <div>expires: {moment.utc(expires).fromNow()}</div>
423 case ModlogActionType.ModAddCommunity: {
425 mod_add_community: { removed },
428 } = view as ModAddCommunityView;
432 <span>{removed ? "Removed " : "Appointed "}</span>
434 <PersonListing person={modded_person} />
436 <span> as a mod to the community </span>
438 <CommunityLink community={community} />
444 case ModlogActionType.ModTransferCommunity: {
446 mod_transfer_community: { removed },
449 } = view as ModTransferCommunityView;
453 <span>{removed ? "Removed " : "Transferred "}</span>
455 <CommunityLink community={community} />
459 <PersonListing person={modded_person} />
465 case ModlogActionType.ModBan: {
467 mod_ban: { reason, expires, banned },
469 } = view as ModBanView;
473 <span>{banned ? "Banned " : "Unbanned "}</span>
475 <PersonListing person={banned_person} />
479 <div>reason: {reason}</div>
484 <div>expires: {moment.utc(expires).fromNow()}</div>
491 case ModlogActionType.ModAdd: {
493 mod_add: { removed },
495 } = view as ModAddView;
499 <span>{removed ? "Removed " : "Appointed "}</span>
501 <PersonListing person={modded_person} />
503 <span> as an admin </span>
507 case ModlogActionType.AdminPurgePerson: {
509 admin_purge_person: { reason },
510 } = view as AdminPurgePersonView;
514 <span>Purged a Person</span>
517 <div>reason: {reason}</div>
524 case ModlogActionType.AdminPurgeCommunity: {
526 admin_purge_community: { reason },
527 } = view as AdminPurgeCommunityView;
531 <span>Purged a Community</span>
534 <div>reason: {reason}</div>
541 case ModlogActionType.AdminPurgePost: {
543 admin_purge_post: { reason },
545 } = view as AdminPurgePostView;
549 <span>Purged a Post from from </span>
550 <CommunityLink community={community} />
553 <div>reason: {reason}</div>
560 case ModlogActionType.AdminPurgeComment: {
562 admin_purge_comment: { reason },
564 } = view as AdminPurgeCommentView;
569 Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
573 <div>reason: {reason}</div>
593 filterType: FilterType;
594 onChange: (option: Choice) => void;
595 value?: number | null;
596 onSearch: (text: string) => void;
600 <div className="col-sm-6 form-group">
601 <label className="col-form-label" htmlFor={`filter-${filterType}`}>
602 {i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
605 id={`filter-${filterType}`}
609 label: i18n.t("all"),
620 async function createNewOptions({
626 oldOptions: Choice[];
629 const newOptions: Choice[] = [];
632 const selectedUser = oldOptions.find(
633 ({ value }) => value === id.toString()
637 newOptions.push(selectedUser);
641 if (text.length > 0) {
643 ...(await fetchUsers(text)).users
644 .slice(0, fetchLimit)
645 .map<Choice>(personToChoice)
652 export class Modlog extends Component<
653 RouteComponentProps<{ communityId?: string }>,
656 private isoData = setIsoData(this.context);
657 private subscription?: Subscription;
659 state: ModlogState = {
661 loadingModSearch: false,
662 loadingUserSearch: false,
663 userSearchOptions: [],
664 modSearchOptions: [],
668 props: RouteComponentProps<{ communityId?: string }>,
671 super(props, context);
672 this.handlePageChange = this.handlePageChange.bind(this);
673 this.handleUserChange = this.handleUserChange.bind(this);
674 this.handleModChange = this.handleModChange.bind(this);
676 this.parseMessage = this.parseMessage.bind(this);
677 this.subscription = wsSubscribe(this.parseMessage);
679 // Only fetch the data if coming from another route
680 if (this.isoData.path === this.context.router.route.match.url) {
683 res: this.isoData.routeData[0] as GetModlogResponse,
686 const communityRes: GetCommunityResponse | undefined =
687 this.isoData.routeData[1];
689 // Getting the moderators
692 communityMods: communityRes?.moderators,
695 const filteredModRes: GetPersonDetailsResponse | undefined =
696 this.isoData.routeData[2];
697 if (filteredModRes) {
700 modSearchOptions: [personToChoice(filteredModRes.person_view)],
704 const filteredUserRes: GetPersonDetailsResponse | undefined =
705 this.isoData.routeData[3];
706 if (filteredUserRes) {
709 userSearchOptions: [personToChoice(filteredUserRes.person_view)],
713 this.state = { ...this.state, loadingModlog: false };
719 componentWillUnmount() {
721 this.subscription?.unsubscribe();
726 const res = this.state.res;
727 const combined = res ? buildCombined(res) : [];
734 <MomentTime published={i.when_} />
737 {this.amAdminOrMod && i.moderator ? (
738 <PersonListing person={i.moderator} />
740 <div>{this.modOrAdminText(i.moderator)}</div>
743 <td>{renderModlogType(i)}</td>
750 get amAdminOrMod(): boolean {
751 return amAdmin() || amMod(this.state.communityMods);
754 modOrAdminText(person?: PersonSafe): string {
756 this.isoData.site_res.admins.some(
757 ({ person: { id } }) => id === person.id
763 get documentTitle(): string {
764 return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
776 const { actionType, page, modId, userId } = getModlogQueryParams();
779 <div className="container-lg">
781 title={this.documentTitle}
782 path={this.context.router.route.match.url}
787 className="alert alert-warning text-sm-start text-xs-center"
791 icon="alert-triangle"
793 classes="mr-sm-2 mx-auto d-sm-inline d-block"
795 <T i18nKey="modlog_content_warning" class="d-inline">
801 <Link className="text-body" to={`/c/${communityName}`}>
802 /c/{communityName}{" "}
805 <span>{i18n.t("modlog")}</span>
807 <div className="form-row">
810 onChange={linkEvent(this, this.handleFilterActionChange)}
811 className="custom-select col-sm-6"
814 <option disabled aria-hidden="true">
815 {i18n.t("filter_by_action")}
817 <option value={ModlogActionType.All}>{i18n.t("all")}</option>
818 <option value={ModlogActionType.ModRemovePost}>
821 <option value={ModlogActionType.ModLockPost}>
824 <option value={ModlogActionType.ModFeaturePost}>
827 <option value={ModlogActionType.ModRemoveComment}>
830 <option value={ModlogActionType.ModRemoveCommunity}>
833 <option value={ModlogActionType.ModBanFromCommunity}>
834 Banning From Communities
836 <option value={ModlogActionType.ModAddCommunity}>
837 Adding Mod to Community
839 <option value={ModlogActionType.ModTransferCommunity}>
840 Transferring Communities
842 <option value={ModlogActionType.ModAdd}>
845 <option value={ModlogActionType.ModBan}>Banning From Site</option>
848 <div className="form-row mb-2">
851 onChange={this.handleUserChange}
852 onSearch={this.handleSearchUsers}
854 options={userSearchOptions}
855 loading={loadingUserSearch}
857 {!this.isoData.site_res.site_view.local_site
858 .hide_modlog_mod_names && (
861 onChange={this.handleModChange}
862 onSearch={this.handleSearchMods}
864 options={modSearchOptions}
865 loading={loadingModSearch}
869 <div className="table-responsive">
875 <table id="modlog_table" className="table table-sm table-hover">
876 <thead className="pointer">
878 <th> {i18n.t("time")}</th>
879 <th>{i18n.t("mod")}</th>
880 <th>{i18n.t("action")}</th>
886 <Paginator page={page} onChange={this.handlePageChange} />
893 handleFilterActionChange(i: Modlog, event: any) {
895 actionType: ModlogActionType[event.target.value],
900 handlePageChange(page: number) {
901 this.updateUrl({ page });
904 handleUserChange(option: Choice) {
905 this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
908 handleModChange(option: Choice) {
909 this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
912 handleSearchUsers = debounce(async (text: string) => {
913 const { userId } = getModlogQueryParams();
914 const { userSearchOptions } = this.state;
915 this.setState({ loadingUserSearch: true });
917 const newOptions = await createNewOptions({
920 oldOptions: userSearchOptions,
924 userSearchOptions: newOptions,
925 loadingUserSearch: false,
929 handleSearchMods = debounce(async (text: string) => {
930 const { modId } = getModlogQueryParams();
931 const { modSearchOptions } = this.state;
932 this.setState({ loadingModSearch: true });
934 const newOptions = await createNewOptions({
937 oldOptions: modSearchOptions,
941 modSearchOptions: newOptions,
942 loadingModSearch: false,
946 updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
949 actionType: urlActionType,
952 } = getModlogQueryParams();
954 const queryParams: QueryParams<ModlogProps> = {
955 page: (page ?? urlPage).toString(),
956 actionType: actionType ?? urlActionType,
957 modId: getUpdatedSearchId(modId, urlModId),
958 userId: getUpdatedSearchId(userId, urlUserId),
961 const communityId = this.props.match.params.communityId;
963 this.props.history.push(
964 `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
978 const auth = myAuth(false);
979 const { actionType, page, modId, userId } = getModlogQueryParams();
980 const { communityId: urlCommunityId } = this.props.match.params;
981 const communityId = getIdFromString(urlCommunityId);
983 const modlogForm: GetModlog = {
984 community_id: communityId,
988 other_person_id: userId ?? undefined,
989 mod_person_id: !this.isoData.site_res.site_view.local_site
990 .hide_modlog_mod_names
996 WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
999 const communityForm: GetCommunity = {
1004 WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
1008 static fetchInitialData({
1011 query: { modId: urlModId, page, userId: urlUserId, actionType },
1014 }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<any>[] {
1015 const pathSplit = path.split("/");
1016 const promises: Promise<any>[] = [];
1017 const communityId = getIdFromString(pathSplit[2]);
1018 const modId = !site.site_view.local_site.hide_modlog_mod_names
1019 ? getIdFromString(urlModId)
1021 const userId = getIdFromString(urlUserId);
1023 const modlogForm: GetModlog = {
1024 page: getPageFromString(page),
1026 community_id: communityId,
1027 type_: getActionFromString(actionType),
1028 mod_person_id: modId,
1029 other_person_id: userId,
1033 promises.push(client.getModlog(modlogForm));
1036 const communityForm: GetCommunity = {
1040 promises.push(client.getCommunity(communityForm));
1042 promises.push(Promise.resolve());
1046 const getPersonForm: GetPersonDetails = {
1051 promises.push(client.getPersonDetails(getPersonForm));
1053 promises.push(Promise.resolve());
1057 const getPersonForm: GetPersonDetails = {
1062 promises.push(client.getPersonDetails(getPersonForm));
1064 promises.push(Promise.resolve());
1070 parseMessage(msg: any) {
1071 const op = wsUserOp(msg);
1075 toast(i18n.t(msg.error), "danger");
1078 case UserOperation.GetModlog: {
1079 const res = wsJsonToRes<GetModlogResponse>(msg);
1080 window.scrollTo(0, 0);
1081 this.setState({ res, loadingModlog: false });
1086 case UserOperation.GetCommunity: {
1090 community: { name },
1092 } = wsJsonToRes<GetCommunityResponse>(msg);
1094 communityMods: moderators,
1095 communityName: name,