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,
25 ModRemoveCommunityView,
27 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";
61 import { HtmlTags } from "./common/html-tags";
62 import { Icon, Spinner } from "./common/icon";
63 import { MomentTime } from "./common/moment-time";
64 import { Paginator } from "./common/paginator";
65 import { SearchableSelect } from "./common/searchable-select";
66 import { CommunityLink } from "./community/community-link";
67 import { PersonListing } from "./person/person-listing";
69 type FilterType = "mod" | "user";
75 | ModRemoveCommentView
76 | ModRemoveCommunityView
77 | ModBanFromCommunityView
80 | ModTransferCommunityView
82 | AdminPurgePersonView
83 | AdminPurgeCommunityView
85 | AdminPurgeCommentView;
87 interface ModlogData {
88 modlogResponse: GetModlogResponse;
89 communityResponse?: GetCommunityResponse;
90 modUserResponse?: GetPersonDetailsResponse;
91 userResponse?: GetPersonDetailsResponse;
94 interface ModlogType {
96 type_: ModlogActionType;
102 const getModlogQueryParams = () =>
103 getQueryParams<ModlogProps>({
104 actionType: getActionFromString,
105 modId: getIdFromString,
106 userId: getIdFromString,
107 page: getPageFromString,
110 interface ModlogState {
111 res?: GetModlogResponse;
112 communityMods?: CommunityModeratorView[];
113 communityName?: string;
114 loadingModlog: boolean;
115 loadingModSearch: boolean;
116 loadingUserSearch: boolean;
117 modSearchOptions: Choice[];
118 userSearchOptions: Choice[];
121 interface ModlogProps {
123 userId?: number | null;
124 modId?: number | null;
125 actionType: ModlogActionType;
128 function getActionFromString(action?: string): ModlogActionType {
129 return action !== undefined ? (action as ModlogActionType) : "All";
132 const getModlogActionMapper =
134 actionType: ModlogActionType,
135 getAction: (view: View) => { id: number; when_: string }
137 (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
138 const { id, when_ } = getAction(view);
145 moderator: view.moderator ?? view.admin,
149 function buildCombined({
157 admin_purged_comments,
158 admin_purged_communities,
159 admin_purged_persons,
162 banned_from_community,
163 transferred_to_community,
164 }: GetModlogResponse): ModlogType[] {
165 const combined = removed_posts
167 getModlogActionMapper(
169 ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
174 getModlogActionMapper(
176 ({ mod_lock_post }: ModLockPostView) => mod_lock_post
182 getModlogActionMapper(
184 ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
189 removed_comments.map(
190 getModlogActionMapper(
192 ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
197 removed_communities.map(
198 getModlogActionMapper(
199 "ModRemoveCommunity",
200 ({ mod_remove_community }: ModRemoveCommunityView) =>
206 banned_from_community.map(
207 getModlogActionMapper(
208 "ModBanFromCommunity",
209 ({ mod_ban_from_community }: ModBanFromCommunityView) =>
210 mod_ban_from_community
215 added_to_community.map(
216 getModlogActionMapper(
218 ({ mod_add_community }: ModAddCommunityView) => mod_add_community
223 transferred_to_community.map(
224 getModlogActionMapper(
225 "ModTransferCommunity",
226 ({ mod_transfer_community }: ModTransferCommunityView) =>
227 mod_transfer_community
233 getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add)
238 getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban)
242 admin_purged_persons.map(
243 getModlogActionMapper(
245 ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
250 admin_purged_communities.map(
251 getModlogActionMapper(
252 "AdminPurgeCommunity",
253 ({ admin_purge_community }: AdminPurgeCommunityView) =>
254 admin_purge_community
259 admin_purged_posts.map(
260 getModlogActionMapper(
262 ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
267 admin_purged_comments.map(
268 getModlogActionMapper(
270 ({ admin_purge_comment }: AdminPurgeCommentView) =>
277 combined.sort((a, b) => b.when_.localeCompare(a.when_));
282 function renderModlogType({ type_, view }: ModlogType) {
284 case "ModRemovePost": {
285 const mrpv = view as ModRemovePostView;
287 mod_remove_post: { reason, removed },
293 <span>{removed ? "Removed " : "Restored "}</span>
295 Post <Link to={`/post/${id}`}>{name}</Link>
299 <div>reason: {reason}</div>
306 case "ModLockPost": {
308 mod_lock_post: { locked },
310 } = view as ModLockPostView;
314 <span>{locked ? "Locked " : "Unlocked "}</span>
316 Post <Link to={`/post/${id}`}>{name}</Link>
322 case "ModFeaturePost": {
324 mod_feature_post: { featured, is_featured_community },
326 } = view as ModFeaturePostView;
330 <span>{featured ? "Featured " : "Unfeatured "}</span>
332 Post <Link to={`/post/${id}`}>{name}</Link>
334 <span>{is_featured_community ? " In Community" : " In Local"}</span>
338 case "ModRemoveComment": {
339 const mrc = view as ModRemoveCommentView;
341 mod_remove_comment: { reason, removed },
342 comment: { id, content },
348 <span>{removed ? "Removed " : "Restored "}</span>
350 Comment <Link to={`/comment/${id}`}>{content}</Link>
354 by <PersonListing person={commenter} />
358 <div>reason: {reason}</div>
365 case "ModRemoveCommunity": {
366 const mrco = view as ModRemoveCommunityView;
368 mod_remove_community: { reason, expires, removed },
374 <span>{removed ? "Removed " : "Restored "}</span>
376 Community <CommunityLink community={community} />
380 <div>reason: {reason}</div>
385 <div>expires: {moment.utc(expires).fromNow()}</div>
392 case "ModBanFromCommunity": {
393 const mbfc = view as ModBanFromCommunityView;
395 mod_ban_from_community: { reason, expires, banned },
402 <span>{banned ? "Banned " : "Unbanned "}</span>
404 <PersonListing person={banned_person} />
406 <span> from the community </span>
408 <CommunityLink community={community} />
412 <div>reason: {reason}</div>
417 <div>expires: {moment.utc(expires).fromNow()}</div>
424 case "ModAddCommunity": {
426 mod_add_community: { removed },
429 } = view as ModAddCommunityView;
433 <span>{removed ? "Removed " : "Appointed "}</span>
435 <PersonListing person={modded_person} />
437 <span> as a mod to the community </span>
439 <CommunityLink community={community} />
445 case "ModTransferCommunity": {
446 const { community, modded_person } = view as ModTransferCommunityView;
450 <span>Transferred</span>
452 <CommunityLink community={community} />
456 <PersonListing person={modded_person} />
464 mod_ban: { reason, expires, banned },
466 } = view as ModBanView;
470 <span>{banned ? "Banned " : "Unbanned "}</span>
472 <PersonListing person={banned_person} />
476 <div>reason: {reason}</div>
481 <div>expires: {moment.utc(expires).fromNow()}</div>
490 mod_add: { removed },
492 } = view as ModAddView;
496 <span>{removed ? "Removed " : "Appointed "}</span>
498 <PersonListing person={modded_person} />
500 <span> as an admin </span>
504 case "AdminPurgePerson": {
506 admin_purge_person: { reason },
507 } = view as AdminPurgePersonView;
511 <span>Purged a Person</span>
514 <div>reason: {reason}</div>
521 case "AdminPurgeCommunity": {
523 admin_purge_community: { reason },
524 } = view as AdminPurgeCommunityView;
528 <span>Purged a Community</span>
531 <div>reason: {reason}</div>
538 case "AdminPurgePost": {
540 admin_purge_post: { reason },
542 } = view as AdminPurgePostView;
546 <span>Purged a Post from from </span>
547 <CommunityLink community={community} />
550 <div>reason: {reason}</div>
557 case "AdminPurgeComment": {
559 admin_purge_comment: { reason },
561 } = view as AdminPurgeCommentView;
566 Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
570 <div>reason: {reason}</div>
590 filterType: FilterType;
591 onChange: (option: Choice) => void;
592 value?: number | null;
593 onSearch: (text: string) => void;
597 <div className="col-sm-6 form-group">
598 <label className="col-form-label" htmlFor={`filter-${filterType}`}>
599 {i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
602 id={`filter-${filterType}`}
606 label: i18n.t("all"),
617 async function createNewOptions({
623 oldOptions: Choice[];
626 const newOptions: Choice[] = [];
629 const selectedUser = oldOptions.find(
630 ({ value }) => value === id.toString()
634 newOptions.push(selectedUser);
638 if (text.length > 0) {
640 ...(await fetchUsers(text)).users
641 .slice(0, Number(fetchLimit))
642 .map<Choice>(personToChoice)
649 export class Modlog extends Component<
650 RouteComponentProps<{ communityId?: string }>,
653 private isoData = setIsoData<ModlogData>(this.context);
654 private subscription?: Subscription;
656 state: ModlogState = {
658 loadingModSearch: false,
659 loadingUserSearch: false,
660 userSearchOptions: [],
661 modSearchOptions: [],
665 props: RouteComponentProps<{ communityId?: string }>,
668 super(props, context);
669 this.handlePageChange = this.handlePageChange.bind(this);
670 this.handleUserChange = this.handleUserChange.bind(this);
671 this.handleModChange = this.handleModChange.bind(this);
673 this.parseMessage = this.parseMessage.bind(this);
674 this.subscription = wsSubscribe(this.parseMessage);
676 // Only fetch the data if coming from another route
677 if (this.isoData.path === this.context.router.route.match.url) {
683 } = this.isoData.routeData;
690 // Getting the moderators
693 communityMods: communityResponse?.moderators,
696 if (modUserResponse) {
699 modSearchOptions: [personToChoice(modUserResponse.person_view)],
706 userSearchOptions: [personToChoice(userResponse.person_view)],
710 this.state = { ...this.state, loadingModlog: false };
716 componentWillUnmount() {
718 this.subscription?.unsubscribe();
723 const res = this.state.res;
724 const combined = res ? buildCombined(res) : [];
731 <MomentTime published={i.when_} />
734 {this.amAdminOrMod && i.moderator ? (
735 <PersonListing person={i.moderator} />
737 <div>{this.modOrAdminText(i.moderator)}</div>
740 <td>{renderModlogType(i)}</td>
747 get amAdminOrMod(): boolean {
748 return amAdmin() || amMod(this.state.communityMods);
751 modOrAdminText(person?: Person): string {
753 this.isoData.site_res.admins.some(
754 ({ person: { id } }) => id === person.id
760 get documentTitle(): string {
761 return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
773 const { actionType, page, modId, userId } = getModlogQueryParams();
776 <div className="container-lg">
778 title={this.documentTitle}
779 path={this.context.router.route.match.url}
784 className="alert alert-warning text-sm-start text-xs-center"
788 icon="alert-triangle"
790 classes="mr-sm-2 mx-auto d-sm-inline d-block"
792 <T i18nKey="modlog_content_warning" class="d-inline">
798 <Link className="text-body" to={`/c/${communityName}`}>
799 /c/{communityName}{" "}
802 <span>{i18n.t("modlog")}</span>
804 <div className="form-row">
807 onChange={linkEvent(this, this.handleFilterActionChange)}
808 className="custom-select col-sm-6"
811 <option disabled aria-hidden="true">
812 {i18n.t("filter_by_action")}
814 <option value={"All"}>{i18n.t("all")}</option>
815 <option value={"ModRemovePost"}>Removing Posts</option>
816 <option value={"ModLockPost"}>Locking Posts</option>
817 <option value={"ModFeaturePost"}>Featuring Posts</option>
818 <option value={"ModRemoveComment"}>Removing Comments</option>
819 <option value={"ModRemoveCommunity"}>Removing Communities</option>
820 <option value={"ModBanFromCommunity"}>
821 Banning From Communities
823 <option value={"ModAddCommunity"}>Adding Mod to Community</option>
824 <option value={"ModTransferCommunity"}>
825 Transferring Communities
827 <option value={"ModAdd"}>Adding Mod to Site</option>
828 <option value={"ModBan"}>Banning From Site</option>
831 <div className="form-row mb-2">
834 onChange={this.handleUserChange}
835 onSearch={this.handleSearchUsers}
837 options={userSearchOptions}
838 loading={loadingUserSearch}
840 {!this.isoData.site_res.site_view.local_site
841 .hide_modlog_mod_names && (
844 onChange={this.handleModChange}
845 onSearch={this.handleSearchMods}
847 options={modSearchOptions}
848 loading={loadingModSearch}
852 <div className="table-responsive">
858 <table id="modlog_table" className="table table-sm table-hover">
859 <thead className="pointer">
861 <th> {i18n.t("time")}</th>
862 <th>{i18n.t("mod")}</th>
863 <th>{i18n.t("action")}</th>
869 <Paginator page={page} onChange={this.handlePageChange} />
876 handleFilterActionChange(i: Modlog, event: any) {
878 actionType: event.target.value as ModlogActionType,
883 handlePageChange(page: number) {
884 this.updateUrl({ page });
887 handleUserChange(option: Choice) {
888 this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
891 handleModChange(option: Choice) {
892 this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
895 handleSearchUsers = debounce(async (text: string) => {
896 const { userId } = getModlogQueryParams();
897 const { userSearchOptions } = this.state;
898 this.setState({ loadingUserSearch: true });
900 const newOptions = await createNewOptions({
903 oldOptions: userSearchOptions,
907 userSearchOptions: newOptions,
908 loadingUserSearch: false,
912 handleSearchMods = debounce(async (text: string) => {
913 const { modId } = getModlogQueryParams();
914 const { modSearchOptions } = this.state;
915 this.setState({ loadingModSearch: true });
917 const newOptions = await createNewOptions({
920 oldOptions: modSearchOptions,
924 modSearchOptions: newOptions,
925 loadingModSearch: false,
929 updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
932 actionType: urlActionType,
935 } = getModlogQueryParams();
937 const queryParams: QueryParams<ModlogProps> = {
938 page: (page ?? urlPage).toString(),
939 actionType: actionType ?? urlActionType,
940 modId: getUpdatedSearchId(modId, urlModId),
941 userId: getUpdatedSearchId(userId, urlUserId),
944 const communityId = this.props.match.params.communityId;
946 this.props.history.push(
947 `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
961 const auth = myAuth(false);
962 const { actionType, page, modId, userId } = getModlogQueryParams();
963 const { communityId: urlCommunityId } = this.props.match.params;
964 const communityId = getIdFromString(urlCommunityId);
966 const modlogForm: GetModlog = {
967 community_id: communityId,
971 other_person_id: userId ?? undefined,
972 mod_person_id: !this.isoData.site_res.site_view.local_site
973 .hide_modlog_mod_names
979 WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
982 const communityForm: GetCommunity = {
987 WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
991 static fetchInitialData({
994 query: { modId: urlModId, page, userId: urlUserId, actionType },
997 }: InitialFetchRequest<
998 QueryParams<ModlogProps>
999 >): WithPromiseKeys<ModlogData> {
1000 const pathSplit = path.split("/");
1001 const communityId = getIdFromString(pathSplit[2]);
1002 const modId = !site.site_view.local_site.hide_modlog_mod_names
1003 ? getIdFromString(urlModId)
1005 const userId = getIdFromString(urlUserId);
1007 const modlogForm: GetModlog = {
1008 page: getPageFromString(page),
1010 community_id: communityId,
1011 type_: getActionFromString(actionType),
1012 mod_person_id: modId,
1013 other_person_id: userId,
1017 let communityResponse: Promise<GetCommunityResponse> | undefined =
1021 const communityForm: GetCommunity = {
1026 communityResponse = client.getCommunity(communityForm);
1029 let modUserResponse: Promise<GetPersonDetailsResponse> | undefined =
1033 const getPersonForm: GetPersonDetails = {
1038 modUserResponse = client.getPersonDetails(getPersonForm);
1041 let userResponse: Promise<GetPersonDetailsResponse> | undefined = undefined;
1044 const getPersonForm: GetPersonDetails = {
1049 userResponse = client.getPersonDetails(getPersonForm);
1053 modlogResponse: client.getModlog(modlogForm),
1060 parseMessage(msg: any) {
1061 const op = wsUserOp(msg);
1065 toast(i18n.t(msg.error), "danger");
1068 case UserOperation.GetModlog: {
1069 const res = wsJsonToRes<GetModlogResponse>(msg);
1070 window.scrollTo(0, 0);
1071 this.setState({ res, loadingModlog: false });
1076 case UserOperation.GetCommunity: {
1080 community: { name },
1082 } = wsJsonToRes<GetCommunityResponse>(msg);
1084 communityMods: moderators,
1085 communityName: name,