-import { Component, linkEvent } from 'inferno';
-import { Link } from 'inferno-router';
-import { Subscription } from 'rxjs';
+import { NoOptionI18nKeys } from "i18next";
+import { Component, linkEvent } from "inferno";
+import { T } from "inferno-i18next-dess";
+import { Link } from "inferno-router";
+import { RouteComponentProps } from "inferno-router/dist/Route";
import {
- UserOperation,
- GetModlogForm,
+ AdminPurgeCommentView,
+ AdminPurgeCommunityView,
+ AdminPurgePersonView,
+ AdminPurgePostView,
+ GetCommunity,
+ GetCommunityResponse,
+ GetModlog,
GetModlogResponse,
- ModRemovePost,
- ModLockPost,
- ModStickyPost,
- ModRemoveComment,
- ModRemoveCommunity,
- ModBanFromCommunity,
- ModBan,
- ModAddCommunity,
- ModAdd,
- WebSocketJsonResponse,
- Site,
-} from 'lemmy-js-client';
-import { WebSocketService } from '../services';
+ GetPersonDetails,
+ GetPersonDetailsResponse,
+ ModAddCommunityView,
+ ModAddView,
+ ModBanFromCommunityView,
+ ModBanView,
+ ModFeaturePostView,
+ ModLockPostView,
+ ModRemoveCommentView,
+ ModRemoveCommunityView,
+ ModRemovePostView,
+ ModTransferCommunityView,
+ ModlogActionType,
+ Person,
+} from "lemmy-js-client";
+import moment from "moment";
+import { i18n } from "../i18next";
+import { InitialFetchRequest } from "../interfaces";
+import { FirstLoadService } from "../services/FirstLoadService";
+import { HttpService, RequestState } from "../services/HttpService";
import {
- wsJsonToRes,
- addTypeInfo,
+ Choice,
+ QueryParams,
+ RouteDataResponse,
+ amAdmin,
+ amMod,
+ debounce,
fetchLimit,
- toast,
+ fetchUsers,
+ getIdFromString,
+ getPageFromString,
+ getQueryParams,
+ getQueryString,
+ getUpdatedSearchId,
+ myAuth,
+ personToChoice,
setIsoData,
- wsSubscribe,
- isBrowser,
-} from '../utils';
-import { MomentTime } from './moment-time';
-import { HtmlTags } from './html-tags';
-import moment from 'moment';
-import { i18n } from '../i18next';
-import { InitialFetchRequest } from 'shared/interfaces';
+} from "../utils";
+import { HtmlTags } from "./common/html-tags";
+import { Icon, Spinner } from "./common/icon";
+import { MomentTime } from "./common/moment-time";
+import { Paginator } from "./common/paginator";
+import { SearchableSelect } from "./common/searchable-select";
+import { CommunityLink } from "./community/community-link";
+import { PersonListing } from "./person/person-listing";
+
+type FilterType = "mod" | "user";
+
+type View =
+ | ModRemovePostView
+ | ModLockPostView
+ | ModFeaturePostView
+ | ModRemoveCommentView
+ | ModRemoveCommunityView
+ | ModBanFromCommunityView
+ | ModBanView
+ | ModAddCommunityView
+ | ModTransferCommunityView
+ | ModAddView
+ | AdminPurgePersonView
+ | AdminPurgeCommunityView
+ | AdminPurgePostView
+ | AdminPurgeCommentView;
+
+type ModlogData = RouteDataResponse<{
+ res: GetModlogResponse;
+ communityRes: GetCommunityResponse;
+ modUserResponse: GetPersonDetailsResponse;
+ userResponse: GetPersonDetailsResponse;
+}>;
+
+interface ModlogType {
+ id: number;
+ type_: ModlogActionType;
+ moderator?: Person;
+ view: View;
+ when_: string;
+}
+
+const getModlogQueryParams = () =>
+ getQueryParams<ModlogProps>({
+ actionType: getActionFromString,
+ modId: getIdFromString,
+ userId: getIdFromString,
+ page: getPageFromString,
+ });
interface ModlogState {
- combined: {
- type_: string;
- data:
- | ModRemovePost
- | ModLockPost
- | ModStickyPost
- | ModRemoveCommunity
- | ModAdd
- | ModBan;
- }[];
- communityId?: number;
- communityName?: string;
+ res: RequestState<GetModlogResponse>;
+ communityRes: RequestState<GetCommunityResponse>;
+ loadingModSearch: boolean;
+ loadingUserSearch: boolean;
+ modSearchOptions: Choice[];
+ userSearchOptions: Choice[];
+}
+
+interface ModlogProps {
page: number;
- site: Site;
- loading: boolean;
+ userId?: number | null;
+ modId?: number | null;
+ actionType: ModlogActionType;
}
-export class Modlog extends Component<any, ModlogState> {
- private isoData = setIsoData(this.context);
- private subscription: Subscription;
- private emptyState: ModlogState = {
- combined: [],
- page: 1,
- loading: true,
- site: this.isoData.site.site,
+function getActionFromString(action?: string): ModlogActionType {
+ return action !== undefined ? (action as ModlogActionType) : "All";
+}
+
+const getModlogActionMapper =
+ (
+ actionType: ModlogActionType,
+ getAction: (view: View) => { id: number; when_: string }
+ ) =>
+ (view: View & { moderator?: Person; admin?: Person }): ModlogType => {
+ const { id, when_ } = getAction(view);
+
+ return {
+ id,
+ type_: actionType,
+ view,
+ when_,
+ moderator: view.moderator ?? view.admin,
+ };
};
- constructor(props: any, context: any) {
- super(props, context);
+function buildCombined({
+ removed_comments,
+ locked_posts,
+ featured_posts,
+ removed_communities,
+ removed_posts,
+ added,
+ added_to_community,
+ admin_purged_comments,
+ admin_purged_communities,
+ admin_purged_persons,
+ admin_purged_posts,
+ banned,
+ banned_from_community,
+ transferred_to_community,
+}: GetModlogResponse): ModlogType[] {
+ const combined = removed_posts
+ .map(
+ getModlogActionMapper(
+ "ModRemovePost",
+ ({ mod_remove_post }: ModRemovePostView) => mod_remove_post
+ )
+ )
+ .concat(
+ locked_posts.map(
+ getModlogActionMapper(
+ "ModLockPost",
+ ({ mod_lock_post }: ModLockPostView) => mod_lock_post
+ )
+ )
+ )
+ .concat(
+ featured_posts.map(
+ getModlogActionMapper(
+ "ModFeaturePost",
+ ({ mod_feature_post }: ModFeaturePostView) => mod_feature_post
+ )
+ )
+ )
+ .concat(
+ removed_comments.map(
+ getModlogActionMapper(
+ "ModRemoveComment",
+ ({ mod_remove_comment }: ModRemoveCommentView) => mod_remove_comment
+ )
+ )
+ )
+ .concat(
+ removed_communities.map(
+ getModlogActionMapper(
+ "ModRemoveCommunity",
+ ({ mod_remove_community }: ModRemoveCommunityView) =>
+ mod_remove_community
+ )
+ )
+ )
+ .concat(
+ banned_from_community.map(
+ getModlogActionMapper(
+ "ModBanFromCommunity",
+ ({ mod_ban_from_community }: ModBanFromCommunityView) =>
+ mod_ban_from_community
+ )
+ )
+ )
+ .concat(
+ added_to_community.map(
+ getModlogActionMapper(
+ "ModAddCommunity",
+ ({ mod_add_community }: ModAddCommunityView) => mod_add_community
+ )
+ )
+ )
+ .concat(
+ transferred_to_community.map(
+ getModlogActionMapper(
+ "ModTransferCommunity",
+ ({ mod_transfer_community }: ModTransferCommunityView) =>
+ mod_transfer_community
+ )
+ )
+ )
+ .concat(
+ added.map(
+ getModlogActionMapper("ModAdd", ({ mod_add }: ModAddView) => mod_add)
+ )
+ )
+ .concat(
+ banned.map(
+ getModlogActionMapper("ModBan", ({ mod_ban }: ModBanView) => mod_ban)
+ )
+ )
+ .concat(
+ admin_purged_persons.map(
+ getModlogActionMapper(
+ "AdminPurgePerson",
+ ({ admin_purge_person }: AdminPurgePersonView) => admin_purge_person
+ )
+ )
+ )
+ .concat(
+ admin_purged_communities.map(
+ getModlogActionMapper(
+ "AdminPurgeCommunity",
+ ({ admin_purge_community }: AdminPurgeCommunityView) =>
+ admin_purge_community
+ )
+ )
+ )
+ .concat(
+ admin_purged_posts.map(
+ getModlogActionMapper(
+ "AdminPurgePost",
+ ({ admin_purge_post }: AdminPurgePostView) => admin_purge_post
+ )
+ )
+ )
+ .concat(
+ admin_purged_comments.map(
+ getModlogActionMapper(
+ "AdminPurgeComment",
+ ({ admin_purge_comment }: AdminPurgeCommentView) =>
+ admin_purge_comment
+ )
+ )
+ );
- this.state = this.emptyState;
- this.state.communityId = this.props.match.params.community_id
- ? Number(this.props.match.params.community_id)
- : undefined;
+ // Sort them by time
+ combined.sort((a, b) => b.when_.localeCompare(a.when_));
+
+ return combined;
+}
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+function renderModlogType({ type_, view }: ModlogType) {
+ switch (type_) {
+ case "ModRemovePost": {
+ const mrpv = view as ModRemovePostView;
+ const {
+ mod_remove_post: { reason, removed },
+ post: { name, id },
+ } = mrpv;
- // Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- let data = this.isoData.routeData[0];
- this.setCombined(data);
- this.state.loading = false;
- } else {
- this.refetch();
+ return (
+ <>
+ <span>{removed ? "Removed " : "Restored "}</span>
+ <span>
+ Post <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModLockPost": {
+ const {
+ mod_lock_post: { locked },
+ post: { id, name },
+ } = view as ModLockPostView;
+
+ return (
+ <>
+ <span>{locked ? "Locked " : "Unlocked "}</span>
+ <span>
+ Post <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ </>
+ );
+ }
+
+ case "ModFeaturePost": {
+ const {
+ mod_feature_post: { featured, is_featured_community },
+ post: { id, name },
+ } = view as ModFeaturePostView;
+
+ return (
+ <>
+ <span>{featured ? "Featured " : "Unfeatured "}</span>
+ <span>
+ Post <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ <span>{is_featured_community ? " In Community" : " In Local"}</span>
+ </>
+ );
+ }
+ case "ModRemoveComment": {
+ const mrc = view as ModRemoveCommentView;
+ const {
+ mod_remove_comment: { reason, removed },
+ comment: { id, content },
+ commenter,
+ } = mrc;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Restored "}</span>
+ <span>
+ Comment <Link to={`/comment/${id}`}>{content}</Link>
+ </span>
+ <span>
+ {" "}
+ by <PersonListing person={commenter} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModRemoveCommunity": {
+ const mrco = view as ModRemoveCommunityView;
+ const {
+ mod_remove_community: { reason, expires, removed },
+ community,
+ } = mrco;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Restored "}</span>
+ <span>
+ Community <CommunityLink community={community} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ {expires && (
+ <span>
+ <div>expires: {moment.utc(expires).fromNow()}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModBanFromCommunity": {
+ const mbfc = view as ModBanFromCommunityView;
+ const {
+ mod_ban_from_community: { reason, expires, banned },
+ banned_person,
+ community,
+ } = mbfc;
+
+ return (
+ <>
+ <span>{banned ? "Banned " : "Unbanned "}</span>
+ <span>
+ <PersonListing person={banned_person} />
+ </span>
+ <span> from the community </span>
+ <span>
+ <CommunityLink community={community} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ {expires && (
+ <span>
+ <div>expires: {moment.utc(expires).fromNow()}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModAddCommunity": {
+ const {
+ mod_add_community: { removed },
+ modded_person,
+ community,
+ } = view as ModAddCommunityView;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Appointed "}</span>
+ <span>
+ <PersonListing person={modded_person} />
+ </span>
+ <span> as a mod to the community </span>
+ <span>
+ <CommunityLink community={community} />
+ </span>
+ </>
+ );
}
- }
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription.unsubscribe();
+ case "ModTransferCommunity": {
+ const { community, modded_person } = view as ModTransferCommunityView;
+
+ return (
+ <>
+ <span>Transferred</span>
+ <span>
+ <CommunityLink community={community} />
+ </span>
+ <span> to </span>
+ <span>
+ <PersonListing person={modded_person} />
+ </span>
+ </>
+ );
+ }
+
+ case "ModBan": {
+ const {
+ mod_ban: { reason, expires, banned },
+ banned_person,
+ } = view as ModBanView;
+
+ return (
+ <>
+ <span>{banned ? "Banned " : "Unbanned "}</span>
+ <span>
+ <PersonListing person={banned_person} />
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ {expires && (
+ <span>
+ <div>expires: {moment.utc(expires).fromNow()}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "ModAdd": {
+ const {
+ mod_add: { removed },
+ modded_person,
+ } = view as ModAddView;
+
+ return (
+ <>
+ <span>{removed ? "Removed " : "Appointed "}</span>
+ <span>
+ <PersonListing person={modded_person} />
+ </span>
+ <span> as an admin </span>
+ </>
+ );
}
+ case "AdminPurgePerson": {
+ const {
+ admin_purge_person: { reason },
+ } = view as AdminPurgePersonView;
+
+ return (
+ <>
+ <span>Purged a Person</span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "AdminPurgeCommunity": {
+ const {
+ admin_purge_community: { reason },
+ } = view as AdminPurgeCommunityView;
+
+ return (
+ <>
+ <span>Purged a Community</span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "AdminPurgePost": {
+ const {
+ admin_purge_post: { reason },
+ community,
+ } = view as AdminPurgePostView;
+
+ return (
+ <>
+ <span>Purged a Post from from </span>
+ <CommunityLink community={community} />
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ case "AdminPurgeComment": {
+ const {
+ admin_purge_comment: { reason },
+ post: { id, name },
+ } = view as AdminPurgeCommentView;
+
+ return (
+ <>
+ <span>
+ Purged a Comment from <Link to={`/post/${id}`}>{name}</Link>
+ </span>
+ {reason && (
+ <span>
+ <div>reason: {reason}</div>
+ </span>
+ )}
+ </>
+ );
+ }
+
+ default:
+ return <></>;
}
+}
- setCombined(res: GetModlogResponse) {
- let removed_posts = addTypeInfo(res.removed_posts, 'removed_posts');
- let locked_posts = addTypeInfo(res.locked_posts, 'locked_posts');
- let stickied_posts = addTypeInfo(res.stickied_posts, 'stickied_posts');
- let removed_comments = addTypeInfo(
- res.removed_comments,
- 'removed_comments'
- );
- let removed_communities = addTypeInfo(
- res.removed_communities,
- 'removed_communities'
- );
- let banned_from_community = addTypeInfo(
- res.banned_from_community,
- 'banned_from_community'
- );
- let added_to_community = addTypeInfo(
- res.added_to_community,
- 'added_to_community'
+const Filter = ({
+ filterType,
+ onChange,
+ value,
+ onSearch,
+ options,
+ loading,
+}: {
+ filterType: FilterType;
+ onChange: (option: Choice) => void;
+ value?: number | null;
+ onSearch: (text: string) => void;
+ options: Choice[];
+ loading: boolean;
+}) => (
+ <div className="col-sm-6 mb-3">
+ <label className="mb-2" htmlFor={`filter-${filterType}`}>
+ {i18n.t(`filter_by_${filterType}` as NoOptionI18nKeys)}
+ </label>
+ <SearchableSelect
+ id={`filter-${filterType}`}
+ value={value ?? 0}
+ options={[
+ {
+ label: i18n.t("all"),
+ value: "0",
+ },
+ ].concat(options)}
+ onChange={onChange}
+ onSearch={onSearch}
+ loading={loading}
+ />
+ </div>
+);
+
+async function createNewOptions({
+ id,
+ oldOptions,
+ text,
+}: {
+ id?: number | null;
+ oldOptions: Choice[];
+ text: string;
+}) {
+ const newOptions: Choice[] = [];
+
+ if (id) {
+ const selectedUser = oldOptions.find(
+ ({ value }) => value === id.toString()
);
- let added = addTypeInfo(res.added, 'added');
- let banned = addTypeInfo(res.banned, 'banned');
- this.state.combined = [];
-
- this.state.combined.push(...removed_posts);
- this.state.combined.push(...locked_posts);
- this.state.combined.push(...stickied_posts);
- this.state.combined.push(...removed_comments);
- this.state.combined.push(...removed_communities);
- this.state.combined.push(...banned_from_community);
- this.state.combined.push(...added_to_community);
- this.state.combined.push(...added);
- this.state.combined.push(...banned);
-
- if (this.state.communityId && this.state.combined.length > 0) {
- this.state.communityName = (this.state.combined[0]
- .data as ModRemovePost).community_name;
+
+ if (selectedUser) {
+ newOptions.push(selectedUser);
}
+ }
- // Sort them by time
- this.state.combined.sort((a, b) =>
- b.data.when_.localeCompare(a.data.when_)
+ if (text.length > 0) {
+ newOptions.push(
+ ...(await fetchUsers(text))
+ .slice(0, Number(fetchLimit))
+ .map<Choice>(personToChoice)
);
}
- combined() {
+ return newOptions;
+}
+
+export class Modlog extends Component<
+ RouteComponentProps<{ communityId?: string }>,
+ ModlogState
+> {
+ private isoData = setIsoData<ModlogData>(this.context);
+
+ state: ModlogState = {
+ res: { state: "empty" },
+ communityRes: { state: "empty" },
+ loadingModSearch: false,
+ loadingUserSearch: false,
+ userSearchOptions: [],
+ modSearchOptions: [],
+ };
+
+ constructor(
+ props: RouteComponentProps<{ communityId?: string }>,
+ context: any
+ ) {
+ super(props, context);
+ this.handlePageChange = this.handlePageChange.bind(this);
+ this.handleUserChange = this.handleUserChange.bind(this);
+ this.handleModChange = this.handleModChange.bind(this);
+
+ // Only fetch the data if coming from another route
+ if (FirstLoadService.isFirstLoad) {
+ const { res, communityRes, modUserResponse, userResponse } =
+ this.isoData.routeData;
+
+ this.state = {
+ ...this.state,
+ res,
+ communityRes,
+ };
+
+ if (modUserResponse.state === "success") {
+ this.state = {
+ ...this.state,
+ modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
+ };
+ }
+
+ if (userResponse.state === "success") {
+ this.state = {
+ ...this.state,
+ userSearchOptions: [personToChoice(userResponse.data.person_view)],
+ };
+ }
+ }
+ }
+
+ get combined() {
+ const res = this.state.res;
+ const combined = res.state == "success" ? buildCombined(res.data) : [];
+
return (
<tbody>
- {this.state.combined.map(i => (
- <tr>
+ {combined.map(i => (
+ <tr key={i.id}>
<td>
- <MomentTime data={i.data} />
+ <MomentTime published={i.when_} />
</td>
<td>
- <Link to={`/u/${i.data.mod_user_name}`}>
- {i.data.mod_user_name}
- </Link>
- </td>
- <td>
- {i.type_ == 'removed_posts' && (
- <>
- {(i.data as ModRemovePost).removed ? 'Removed' : 'Restored'}
- <span>
- {' '}
- Post{' '}
- <Link to={`/post/${(i.data as ModRemovePost).post_id}`}>
- {(i.data as ModRemovePost).post_name}
- </Link>
- </span>
- <div>
- {(i.data as ModRemovePost).reason &&
- ` reason: ${(i.data as ModRemovePost).reason}`}
- </div>
- </>
- )}
- {i.type_ == 'locked_posts' && (
- <>
- {(i.data as ModLockPost).locked ? 'Locked' : 'Unlocked'}
- <span>
- {' '}
- Post{' '}
- <Link to={`/post/${(i.data as ModLockPost).post_id}`}>
- {(i.data as ModLockPost).post_name}
- </Link>
- </span>
- </>
- )}
- {i.type_ == 'stickied_posts' && (
- <>
- {(i.data as ModStickyPost).stickied
- ? 'Stickied'
- : 'Unstickied'}
- <span>
- {' '}
- Post{' '}
- <Link to={`/post/${(i.data as ModStickyPost).post_id}`}>
- {(i.data as ModStickyPost).post_name}
- </Link>
- </span>
- </>
- )}
- {i.type_ == 'removed_comments' && (
- <>
- {(i.data as ModRemoveComment).removed
- ? 'Removed'
- : 'Restored'}
- <span>
- {' '}
- Comment{' '}
- <Link
- to={`/post/${
- (i.data as ModRemoveComment).post_id
- }/comment/${(i.data as ModRemoveComment).comment_id}`}
- >
- {(i.data as ModRemoveComment).comment_content}
- </Link>
- </span>
- <span>
- {' '}
- by{' '}
- <Link
- to={`/u/${
- (i.data as ModRemoveComment).comment_user_name
- }`}
- >
- {(i.data as ModRemoveComment).comment_user_name}
- </Link>
- </span>
- <div>
- {(i.data as ModRemoveComment).reason &&
- ` reason: ${(i.data as ModRemoveComment).reason}`}
- </div>
- </>
- )}
- {i.type_ == 'removed_communities' && (
- <>
- {(i.data as ModRemoveCommunity).removed
- ? 'Removed'
- : 'Restored'}
- <span>
- {' '}
- Community{' '}
- <Link
- to={`/c/${(i.data as ModRemoveCommunity).community_name}`}
- >
- {(i.data as ModRemoveCommunity).community_name}
- </Link>
- </span>
- <div>
- {(i.data as ModRemoveCommunity).reason &&
- ` reason: ${(i.data as ModRemoveCommunity).reason}`}
- </div>
- <div>
- {(i.data as ModRemoveCommunity).expires &&
- ` expires: ${moment
- .utc((i.data as ModRemoveCommunity).expires)
- .fromNow()}`}
- </div>
- </>
- )}
- {i.type_ == 'banned_from_community' && (
- <>
- <span>
- {(i.data as ModBanFromCommunity).banned
- ? 'Banned '
- : 'Unbanned '}{' '}
- </span>
- <span>
- <Link
- to={`/u/${
- (i.data as ModBanFromCommunity).other_user_name
- }`}
- >
- {(i.data as ModBanFromCommunity).other_user_name}
- </Link>
- </span>
- <span> from the community </span>
- <span>
- <Link
- to={`/c/${
- (i.data as ModBanFromCommunity).community_name
- }`}
- >
- {(i.data as ModBanFromCommunity).community_name}
- </Link>
- </span>
- <div>
- {(i.data as ModBanFromCommunity).reason &&
- ` reason: ${(i.data as ModBanFromCommunity).reason}`}
- </div>
- <div>
- {(i.data as ModBanFromCommunity).expires &&
- ` expires: ${moment
- .utc((i.data as ModBanFromCommunity).expires)
- .fromNow()}`}
- </div>
- </>
- )}
- {i.type_ == 'added_to_community' && (
- <>
- <span>
- {(i.data as ModAddCommunity).removed
- ? 'Removed '
- : 'Appointed '}{' '}
- </span>
- <span>
- <Link
- to={`/u/${(i.data as ModAddCommunity).other_user_name}`}
- >
- {(i.data as ModAddCommunity).other_user_name}
- </Link>
- </span>
- <span> as a mod to the community </span>
- <span>
- <Link
- to={`/c/${(i.data as ModAddCommunity).community_name}`}
- >
- {(i.data as ModAddCommunity).community_name}
- </Link>
- </span>
- </>
- )}
- {i.type_ == 'banned' && (
- <>
- <span>
- {(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '}{' '}
- </span>
- <span>
- <Link to={`/u/${(i.data as ModBan).other_user_name}`}>
- {(i.data as ModBan).other_user_name}
- </Link>
- </span>
- <div>
- {(i.data as ModBan).reason &&
- ` reason: ${(i.data as ModBan).reason}`}
- </div>
- <div>
- {(i.data as ModBan).expires &&
- ` expires: ${moment
- .utc((i.data as ModBan).expires)
- .fromNow()}`}
- </div>
- </>
- )}
- {i.type_ == 'added' && (
- <>
- <span>
- {(i.data as ModAdd).removed ? 'Removed ' : 'Appointed '}{' '}
- </span>
- <span>
- <Link to={`/u/${(i.data as ModAdd).other_user_name}`}>
- {(i.data as ModAdd).other_user_name}
- </Link>
- </span>
- <span> as an admin </span>
- </>
+ {this.amAdminOrMod && i.moderator ? (
+ <PersonListing person={i.moderator} />
+ ) : (
+ <div>{this.modOrAdminText(i.moderator)}</div>
)}
</td>
+ <td>{renderModlogType(i)}</td>
</tr>
))}
</tbody>
);
}
+ get amAdminOrMod(): boolean {
+ const amMod_ =
+ this.state.communityRes.state == "success" &&
+ amMod(this.state.communityRes.data.moderators);
+ return amAdmin() || amMod_;
+ }
+
+ modOrAdminText(person?: Person): string {
+ return person &&
+ this.isoData.site_res.admins.some(
+ ({ person: { id } }) => id === person.id
+ )
+ ? i18n.t("admin")
+ : i18n.t("mod");
+ }
+
get documentTitle(): string {
- return `Modlog - ${this.state.site.name}`;
+ return `Modlog - ${this.isoData.site_res.site_view.site.name}`;
}
render() {
+ const {
+ loadingModSearch,
+ loadingUserSearch,
+ userSearchOptions,
+ modSearchOptions,
+ } = this.state;
+ const { actionType, modId, userId } = getModlogQueryParams();
+
return (
- <div class="container">
+ <div className="modlog container-lg">
<HtmlTags
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
- {this.state.loading ? (
- <h5 class="">
- <svg class="icon icon-spinner spin">
- <use xlinkHref="#icon-spinner"></use>
- </svg>
- </h5>
- ) : (
- <div>
+
+ <div>
+ <div
+ className="alert alert-warning text-sm-start text-xs-center"
+ role="alert"
+ >
+ <Icon
+ icon="alert-triangle"
+ inline
+ classes="me-sm-2 mx-auto d-sm-inline d-block"
+ />
+ <T i18nKey="modlog_content_warning" class="d-inline">
+ #<strong>#</strong>#
+ </T>
+ </div>
+ {this.state.communityRes.state === "success" && (
<h5>
- {this.state.communityName && (
- <Link
- className="text-body"
- to={`/c/${this.state.communityName}`}
- >
- /c/{this.state.communityName}{' '}
- </Link>
- )}
- <span>{i18n.t('modlog')}</span>
+ <Link
+ className="text-body"
+ to={`/c/${this.state.communityRes.data.community_view.community.name}`}
+ >
+ /c/{this.state.communityRes.data.community_view.community.name}{" "}
+ </Link>
+ <span>{i18n.t("modlog")}</span>
</h5>
- <div class="table-responsive">
- <table id="modlog_table" class="table table-sm table-hover">
- <thead class="pointer">
- <tr>
- <th> {i18n.t('time')}</th>
- <th>{i18n.t('mod')}</th>
- <th>{i18n.t('action')}</th>
- </tr>
- </thead>
- {this.combined()}
- </table>
- {this.paginator()}
+ )}
+ <div className="row mb-2">
+ <div className="col-sm-6">
+ <select
+ value={actionType}
+ onChange={linkEvent(this, this.handleFilterActionChange)}
+ className="form-select"
+ aria-label="action"
+ >
+ <option disabled aria-hidden="true">
+ {i18n.t("filter_by_action")}
+ </option>
+ <option value={"All"}>{i18n.t("all")}</option>
+ <option value={"ModRemovePost"}>Removing Posts</option>
+ <option value={"ModLockPost"}>Locking Posts</option>
+ <option value={"ModFeaturePost"}>Featuring Posts</option>
+ <option value={"ModRemoveComment"}>Removing Comments</option>
+ <option value={"ModRemoveCommunity"}>
+ Removing Communities
+ </option>
+ <option value={"ModBanFromCommunity"}>
+ Banning From Communities
+ </option>
+ <option value={"ModAddCommunity"}>
+ Adding Mod to Community
+ </option>
+ <option value={"ModTransferCommunity"}>
+ Transferring Communities
+ </option>
+ <option value={"ModAdd"}>Adding Mod to Site</option>
+ <option value={"ModBan"}>Banning From Site</option>
+ </select>
</div>
</div>
- )}
+ <div className="row mb-2">
+ <Filter
+ filterType="user"
+ onChange={this.handleUserChange}
+ onSearch={this.handleSearchUsers}
+ value={userId}
+ options={userSearchOptions}
+ loading={loadingUserSearch}
+ />
+ {!this.isoData.site_res.site_view.local_site
+ .hide_modlog_mod_names && (
+ <Filter
+ filterType="mod"
+ onChange={this.handleModChange}
+ onSearch={this.handleSearchMods}
+ value={modId}
+ options={modSearchOptions}
+ loading={loadingModSearch}
+ />
+ )}
+ </div>
+ {this.renderModlogTable()}
+ </div>
</div>
);
}
- paginator() {
- return (
- <div class="mt-2">
- {this.state.page > 1 && (
- <button
- class="btn btn-secondary mr-1"
- onClick={linkEvent(this, this.prevPage)}
- >
- {i18n.t('prev')}
- </button>
- )}
- <button
- class="btn btn-secondary"
- onClick={linkEvent(this, this.nextPage)}
- >
- {i18n.t('next')}
- </button>
- </div>
- );
+ renderModlogTable() {
+ switch (this.state.res.state) {
+ case "loading":
+ return (
+ <h5>
+ <Spinner large />
+ </h5>
+ );
+ case "success": {
+ const page = getModlogQueryParams().page;
+ return (
+ <div className="table-responsive">
+ <table id="modlog_table" className="table table-sm table-hover">
+ <thead className="pointer">
+ <tr>
+ <th> {i18n.t("time")}</th>
+ <th>{i18n.t("mod")}</th>
+ <th>{i18n.t("action")}</th>
+ </tr>
+ </thead>
+ {this.combined}
+ </table>
+ <Paginator page={page} onChange={this.handlePageChange} />
+ </div>
+ );
+ }
+ }
+ }
+
+ handleFilterActionChange(i: Modlog, event: any) {
+ i.updateUrl({
+ actionType: event.target.value as ModlogActionType,
+ page: 1,
+ });
}
- nextPage(i: Modlog) {
- i.state.page++;
- i.setState(i.state);
- i.refetch();
+ handlePageChange(page: number) {
+ this.updateUrl({ page });
}
- prevPage(i: Modlog) {
- i.state.page--;
- i.setState(i.state);
- i.refetch();
+ handleUserChange(option: Choice) {
+ this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 });
}
- refetch() {
- let modlogForm: GetModlogForm = {
- community_id: this.state.communityId,
- page: this.state.page,
- limit: fetchLimit,
+ handleModChange(option: Choice) {
+ this.updateUrl({ modId: getIdFromString(option.value) ?? null, page: 1 });
+ }
+
+ handleSearchUsers = debounce(async (text: string) => {
+ const { userId } = getModlogQueryParams();
+ const { userSearchOptions } = this.state;
+ this.setState({ loadingUserSearch: true });
+
+ const newOptions = await createNewOptions({
+ id: userId,
+ text,
+ oldOptions: userSearchOptions,
+ });
+
+ this.setState({
+ userSearchOptions: newOptions,
+ loadingUserSearch: false,
+ });
+ });
+
+ handleSearchMods = debounce(async (text: string) => {
+ const { modId } = getModlogQueryParams();
+ const { modSearchOptions } = this.state;
+ this.setState({ loadingModSearch: true });
+
+ const newOptions = await createNewOptions({
+ id: modId,
+ text,
+ oldOptions: modSearchOptions,
+ });
+
+ this.setState({
+ modSearchOptions: newOptions,
+ loadingModSearch: false,
+ });
+ });
+
+ async updateUrl({ actionType, modId, page, userId }: Partial<ModlogProps>) {
+ const {
+ page: urlPage,
+ actionType: urlActionType,
+ modId: urlModId,
+ userId: urlUserId,
+ } = getModlogQueryParams();
+
+ const queryParams: QueryParams<ModlogProps> = {
+ page: (page ?? urlPage).toString(),
+ actionType: actionType ?? urlActionType,
+ modId: getUpdatedSearchId(modId, urlModId),
+ userId: getUpdatedSearchId(userId, urlUserId),
};
- WebSocketService.Instance.getModlog(modlogForm);
+
+ const communityId = this.props.match.params.communityId;
+
+ this.props.history.push(
+ `/modlog${communityId ? `/${communityId}` : ""}${getQueryString(
+ queryParams
+ )}`
+ );
+
+ await this.refetch();
}
- static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
- let pathSplit = req.path.split('/');
- let communityId = pathSplit[3];
- let promises: Promise<any>[] = [];
+ async refetch() {
+ const auth = myAuth();
+ const { actionType, page, modId, userId } = getModlogQueryParams();
+ const { communityId: urlCommunityId } = this.props.match.params;
+ const communityId = getIdFromString(urlCommunityId);
- let modlogForm: GetModlogForm = {
- page: 1,
+ this.setState({ res: { state: "loading" } });
+ this.setState({
+ res: await HttpService.client.getModlog({
+ community_id: communityId,
+ page,
+ limit: fetchLimit,
+ type_: actionType,
+ other_person_id: userId ?? undefined,
+ mod_person_id: !this.isoData.site_res.site_view.local_site
+ .hide_modlog_mod_names
+ ? modId ?? undefined
+ : undefined,
+ auth,
+ }),
+ });
+
+ if (communityId) {
+ this.setState({ communityRes: { state: "loading" } });
+ this.setState({
+ communityRes: await HttpService.client.getCommunity({
+ id: communityId,
+ auth,
+ }),
+ });
+ }
+ }
+
+ static async fetchInitialData({
+ client,
+ path,
+ query: { modId: urlModId, page, userId: urlUserId, actionType },
+ auth,
+ site,
+ }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
+ const pathSplit = path.split("/");
+ const communityId = getIdFromString(pathSplit[2]);
+ const modId = !site.site_view.local_site.hide_modlog_mod_names
+ ? getIdFromString(urlModId)
+ : undefined;
+ const userId = getIdFromString(urlUserId);
+
+ const modlogForm: GetModlog = {
+ page: getPageFromString(page),
limit: fetchLimit,
+ community_id: communityId,
+ type_: getActionFromString(actionType),
+ mod_person_id: modId,
+ other_person_id: userId,
+ auth,
+ };
+
+ let communityResponse: RequestState<GetCommunityResponse> = {
+ state: "empty",
};
if (communityId) {
- modlogForm.community_id = Number(communityId);
+ const communityForm: GetCommunity = {
+ id: communityId,
+ auth,
+ };
+
+ communityResponse = await client.getCommunity(communityForm);
}
- promises.push(req.client.getModlog(modlogForm));
- return promises;
- }
+ let modUserResponse: RequestState<GetPersonDetailsResponse> = {
+ state: "empty",
+ };
+
+ if (modId) {
+ const getPersonForm: GetPersonDetails = {
+ person_id: modId,
+ auth,
+ };
- parseMessage(msg: WebSocketJsonResponse) {
- console.log(msg);
- let res = wsJsonToRes(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), 'danger');
- return;
- } else if (res.op == UserOperation.GetModlog) {
- let data = res.data as GetModlogResponse;
- this.state.loading = false;
- window.scrollTo(0, 0);
- this.setCombined(data);
- this.setState(this.state);
+ modUserResponse = await client.getPersonDetails(getPersonForm);
}
+
+ let userResponse: RequestState<GetPersonDetailsResponse> = {
+ state: "empty",
+ };
+
+ if (userId) {
+ const getPersonForm: GetPersonDetails = {
+ person_id: userId,
+ auth,
+ };
+
+ userResponse = await client.getPersonDetails(getPersonForm);
+ }
+
+ return {
+ res: await client.getModlog(modlogForm),
+ communityRes: communityResponse,
+ modUserResponse,
+ userResponse,
+ };
}
}