import { debounce, getQueryParams, getQueryString } from "@utils/helpers"; import { amAdmin, amMod } from "@utils/roles"; import type { QueryParams } from "@utils/types"; 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 { AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView, AdminPurgePostView, GetCommunity, GetCommunityResponse, GetModlog, GetModlogResponse, 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 { Choice, RouteDataResponse, fetchLimit, fetchUsers, getIdFromString, getPageFromString, getUpdatedSearchId, myAuth, personToChoice, setIsoData, } 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({ actionType: getActionFromString, modId: getIdFromString, userId: getIdFromString, page: getPageFromString, }); interface ModlogState { res: RequestState; communityRes: RequestState; loadingModSearch: boolean; loadingUserSearch: boolean; modSearchOptions: Choice[]; userSearchOptions: Choice[]; } interface ModlogProps { page: number; userId?: number | null; modId?: number | null; actionType: ModlogActionType; } 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, }; }; 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 ) ) ); // Sort them by time combined.sort((a, b) => b.when_.localeCompare(a.when_)); return combined; } function renderModlogType({ type_, view }: ModlogType) { switch (type_) { case "ModRemovePost": { const mrpv = view as ModRemovePostView; const { mod_remove_post: { reason, removed }, post: { name, id }, } = mrpv; return ( <> {removed ? "Removed " : "Restored "} Post {name} {reason && (
reason: {reason}
)} ); } case "ModLockPost": { const { mod_lock_post: { locked }, post: { id, name }, } = view as ModLockPostView; return ( <> {locked ? "Locked " : "Unlocked "} Post {name} ); } case "ModFeaturePost": { const { mod_feature_post: { featured, is_featured_community }, post: { id, name }, } = view as ModFeaturePostView; return ( <> {featured ? "Featured " : "Unfeatured "} Post {name} {is_featured_community ? " In Community" : " In Local"} ); } case "ModRemoveComment": { const mrc = view as ModRemoveCommentView; const { mod_remove_comment: { reason, removed }, comment: { id, content }, commenter, } = mrc; return ( <> {removed ? "Removed " : "Restored "} Comment {content} {" "} by {reason && (
reason: {reason}
)} ); } case "ModRemoveCommunity": { const mrco = view as ModRemoveCommunityView; const { mod_remove_community: { reason, expires, removed }, community, } = mrco; return ( <> {removed ? "Removed " : "Restored "} Community {reason && (
reason: {reason}
)} {expires && (
expires: {moment.utc(expires).fromNow()}
)} ); } case "ModBanFromCommunity": { const mbfc = view as ModBanFromCommunityView; const { mod_ban_from_community: { reason, expires, banned }, banned_person, community, } = mbfc; return ( <> {banned ? "Banned " : "Unbanned "} from the community {reason && (
reason: {reason}
)} {expires && (
expires: {moment.utc(expires).fromNow()}
)} ); } case "ModAddCommunity": { const { mod_add_community: { removed }, modded_person, community, } = view as ModAddCommunityView; return ( <> {removed ? "Removed " : "Appointed "} as a mod to the community ); } case "ModTransferCommunity": { const { community, modded_person } = view as ModTransferCommunityView; return ( <> Transferred to ); } case "ModBan": { const { mod_ban: { reason, expires, banned }, banned_person, } = view as ModBanView; return ( <> {banned ? "Banned " : "Unbanned "} {reason && (
reason: {reason}
)} {expires && (
expires: {moment.utc(expires).fromNow()}
)} ); } case "ModAdd": { const { mod_add: { removed }, modded_person, } = view as ModAddView; return ( <> {removed ? "Removed " : "Appointed "} as an admin ); } case "AdminPurgePerson": { const { admin_purge_person: { reason }, } = view as AdminPurgePersonView; return ( <> Purged a Person {reason && (
reason: {reason}
)} ); } case "AdminPurgeCommunity": { const { admin_purge_community: { reason }, } = view as AdminPurgeCommunityView; return ( <> Purged a Community {reason && (
reason: {reason}
)} ); } case "AdminPurgePost": { const { admin_purge_post: { reason }, community, } = view as AdminPurgePostView; return ( <> Purged a Post from from {reason && (
reason: {reason}
)} ); } case "AdminPurgeComment": { const { admin_purge_comment: { reason }, post: { id, name }, } = view as AdminPurgeCommentView; return ( <> Purged a Comment from {name} {reason && (
reason: {reason}
)} ); } default: return <>; } } 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; }) => (
); 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() ); if (selectedUser) { newOptions.push(selectedUser); } } if (text.length > 0) { newOptions.push( ...(await fetchUsers(text)) .slice(0, Number(fetchLimit)) .map(personToChoice) ); } return newOptions; } export class Modlog extends Component< RouteComponentProps<{ communityId?: string }>, ModlogState > { private isoData = setIsoData(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 ( {combined.map(i => ( {this.amAdminOrMod && i.moderator ? ( ) : (
{this.modOrAdminText(i.moderator)}
)} {renderModlogType(i)} ))} ); } 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.isoData.site_res.site_view.site.name}`; } render() { const { loadingModSearch, loadingUserSearch, userSearchOptions, modSearchOptions, } = this.state; const { actionType, modId, userId } = getModlogQueryParams(); return (
###
{this.state.communityRes.state === "success" && (
/c/{this.state.communityRes.data.community_view.community.name}{" "} {i18n.t("modlog")}
)}
{!this.isoData.site_res.site_view.local_site .hide_modlog_mod_names && ( )}
{this.renderModlogTable()}
); } renderModlogTable() { switch (this.state.res.state) { case "loading": return (
); case "success": { const page = getModlogQueryParams().page; return (
{this.combined}
{i18n.t("time")} {i18n.t("mod")} {i18n.t("action")}
); } } } handleFilterActionChange(i: Modlog, event: any) { i.updateUrl({ actionType: event.target.value as ModlogActionType, page: 1, }); } handlePageChange(page: number) { this.updateUrl({ page }); } handleUserChange(option: Choice) { this.updateUrl({ userId: getIdFromString(option.value) ?? null, page: 1 }); } 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) { const { page: urlPage, actionType: urlActionType, modId: urlModId, userId: urlUserId, } = getModlogQueryParams(); const queryParams: QueryParams = { page: (page ?? urlPage).toString(), actionType: actionType ?? urlActionType, modId: getUpdatedSearchId(modId, urlModId), userId: getUpdatedSearchId(userId, urlUserId), }; const communityId = this.props.match.params.communityId; this.props.history.push( `/modlog${communityId ? `/${communityId}` : ""}${getQueryString( queryParams )}` ); await this.refetch(); } async refetch() { const auth = myAuth(); const { actionType, page, modId, userId } = getModlogQueryParams(); const { communityId: urlCommunityId } = this.props.match.params; const communityId = getIdFromString(urlCommunityId); 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>): Promise { 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 = { state: "empty", }; if (communityId) { const communityForm: GetCommunity = { id: communityId, auth, }; communityResponse = await client.getCommunity(communityForm); } let modUserResponse: RequestState = { state: "empty", }; if (modId) { const getPersonForm: GetPersonDetails = { person_id: modId, auth, }; modUserResponse = await client.getPersonDetails(getPersonForm); } let userResponse: RequestState = { 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, }; } }