Adding purging of comments, posts, communities, and users. (#459)

* Starting on admin purge.

* Updating translations.

* Finishing up item purging.
This commit is contained in:
Dessalines 2022-06-23 15:44:05 -04:00 committed by GitHub
parent 75d52f1e4e
commit 96583bee47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 578 additions and 88 deletions

@ -1 +1 @@
Subproject commit 29c689af8d16417c1b84d9491f6bcea888720a87
Subproject commit de5d4f3a758f8e8b41869c90d97e53ab50577f90

View file

@ -77,7 +77,7 @@
"eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.4",
"import-sort-style-module": "^6.0.0",
"lemmy-js-client": "0.17.0-rc.32",
"lemmy-js-client": "0.17.0-rc.33",
"lint-staged": "^12.4.1",
"mini-css-extract-plugin": "^2.6.0",
"node-fetch": "^2.6.1",

View file

@ -17,6 +17,8 @@ import {
MarkPersonMentionAsRead,
PersonMentionView,
PersonViewSafe,
PurgeComment,
PurgePerson,
RemoveComment,
SaveComment,
toUndefined,
@ -24,7 +26,11 @@ import {
} from "lemmy-js-client";
import moment from "moment";
import { i18n } from "../../i18next";
import { BanType, CommentNode as CommentNodeI } from "../../interfaces";
import {
BanType,
CommentNode as CommentNodeI,
PurgeType,
} from "../../interfaces";
import { UserService, WebSocketService } from "../../services";
import {
amCommunityCreator,
@ -42,7 +48,7 @@ import {
showScores,
wsClient,
} from "../../utils";
import { Icon, Spinner } from "../common/icon";
import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time";
import { CommunityLink } from "../community/community-link";
import { PersonListing } from "../person/person-listing";
@ -59,6 +65,10 @@ interface CommentNodeState {
banReason: Option<string>;
banExpireDays: Option<number>;
banType: BanType;
showPurgeDialog: boolean;
purgeReason: Option<string>;
purgeType: PurgeType;
purgeLoading: boolean;
showConfirmTransferSite: boolean;
showConfirmTransferCommunity: boolean;
showConfirmAppointAsMod: boolean;
@ -102,6 +112,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
banReason: None,
banExpireDays: None,
banType: BanType.Community,
showPurgeDialog: false,
purgeLoading: false,
purgeReason: None,
purgeType: PurgeType.Person,
collapsed: false,
viewSource: false,
showAdvanced: false,
@ -147,6 +161,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let node = this.props.node;
let cv = this.props.node.comment_view;
let purgeTypeText: string;
if (this.state.purgeType == PurgeType.Comment) {
purgeTypeText = i18n.t("purge_comment");
} else if (this.state.purgeType == PurgeType.Person) {
purgeTypeText = `${i18n.t("purge")} ${cv.creator.name}`;
}
let canMod_ = canMod(
this.props.moderators,
this.props.admins,
@ -645,30 +666,54 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{/* Admins can ban from all, and appoint other admins */}
{canAdmin_ && (
<>
{!isAdmin_ &&
(!isBanned(cv.creator) ? (
{!isAdmin_ && (
<>
<button
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
this.handleModBanShow
this.handlePurgePersonShow
)}
aria-label={i18n.t("ban_from_site")}
aria-label={i18n.t("purge_user")}
>
{i18n.t("ban_from_site")}
{i18n.t("purge_user")}
</button>
) : (
<button
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
this.handleModBanSubmit
this.handlePurgeCommentShow
)}
aria-label={i18n.t("unban_from_site")}
aria-label={i18n.t("purge_comment")}
>
{i18n.t("unban_from_site")}
{i18n.t("purge_comment")}
</button>
))}
{!isBanned(cv.creator) ? (
<button
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
this.handleModBanShow
)}
aria-label={i18n.t("ban_from_site")}
>
{i18n.t("ban_from_site")}
</button>
) : (
<button
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
this.handleModBanSubmit
)}
aria-label={i18n.t("unban_from_site")}
>
{i18n.t("unban_from_site")}
</button>
)}
</>
)}
{!isBanned(cv.creator) &&
cv.creator.local &&
(!this.state.showConfirmAppointAsAdmin ? (
@ -848,6 +893,36 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</div>
</form>
)}
{this.state.showPurgeDialog && (
<form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
<PurgeWarning />
<label class="sr-only" htmlFor="purge-reason">
{i18n.t("reason")}
</label>
<input
type="text"
id="purge-reason"
class="form-control my-3"
placeholder={i18n.t("reason")}
value={toUndefined(this.state.purgeReason)}
onInput={linkEvent(this, this.handlePurgeReasonChange)}
/>
<div class="form-group row col-12">
{this.state.purgeLoading ? (
<Spinner />
) : (
<button
type="submit"
class="btn btn-secondary"
aria-label={purgeTypeText}
>
{purgeTypeText}
</button>
)}
</div>
</form>
)}
{this.state.showReply && (
<CommentForm
node={Left(node)}
@ -1202,6 +1277,48 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.setState(i.state);
}
handlePurgePersonShow(i: CommentNode) {
i.state.showPurgeDialog = true;
i.state.purgeType = PurgeType.Person;
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handlePurgeCommentShow(i: CommentNode) {
i.state.showPurgeDialog = true;
i.state.purgeType = PurgeType.Comment;
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handlePurgeReasonChange(i: CommentNode, event: any) {
i.state.purgeReason = Some(event.target.value);
i.setState(i.state);
}
handlePurgeSubmit(i: CommentNode, event: any) {
event.preventDefault();
if (i.state.purgeType == PurgeType.Person) {
let form = new PurgePerson({
person_id: i.props.node.comment_view.creator.id,
reason: i.state.purgeReason,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.purgePerson(form));
} else if (i.state.purgeType == PurgeType.Comment) {
let form = new PurgeComment({
comment_id: i.props.node.comment_view.comment.id,
reason: i.state.purgeReason,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.purgeComment(form));
}
i.state.purgeLoading = true;
i.setState(i.state);
}
handleShowConfirmAppointAsMod(i: CommentNode) {
i.state.showConfirmAppointAsMod = true;
i.setState(i.state);

View file

@ -1,5 +1,6 @@
import classNames from "classnames";
import { Component } from "inferno";
import { i18n } from "../../i18next";
interface IconProps {
icon: string;
@ -48,3 +49,18 @@ export class Spinner extends Component<SpinnerProps, any> {
);
}
}
export class PurgeWarning extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
return (
<div class="mt-2 alert alert-danger" role="alert">
<Icon icon="alert-triangle" classes="icon-inline mr-2" />
{i18n.t("purge_warning")}
</div>
);
}
}

View file

@ -19,6 +19,7 @@ import {
PostReportResponse,
PostResponse,
PostView,
PurgeItemResponse,
SortType,
toOption,
UserOperation,
@ -656,6 +657,12 @@ export class Community extends Component<any, State> {
if (data) {
toast(i18n.t("report_created"));
}
} else if (op == UserOperation.PurgeCommunity) {
let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
if (data.success) {
toast(i18n.t("purge_success"));
this.context.router.history.push(`/`);
}
}
}
}

View file

@ -1,4 +1,4 @@
import { Option, Some } from "@sniptt/monads";
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router";
import {
@ -8,6 +8,7 @@ import {
DeleteCommunity,
FollowCommunity,
PersonViewSafe,
PurgeCommunity,
RemoveCommunity,
SubscribedType,
toUndefined,
@ -25,7 +26,7 @@ import {
wsClient,
} from "../../utils";
import { BannerIconHeader } from "../common/banner-icon-header";
import { Icon } from "../common/icon";
import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { CommunityForm } from "../community/community-form";
import { CommunityLink } from "../community/community-link";
import { PersonListing } from "../person/person-listing";
@ -44,6 +45,9 @@ interface SidebarState {
removeExpires: Option<string>;
showEdit: boolean;
showRemoveDialog: boolean;
showPurgeDialog: boolean;
purgeReason: Option<string>;
purgeLoading: boolean;
showConfirmLeaveModTeam: boolean;
}
@ -51,8 +55,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
private emptyState: SidebarState = {
showEdit: false,
showRemoveDialog: false,
removeReason: null,
removeExpires: null,
removeReason: None,
removeExpires: None,
showPurgeDialog: false,
purgeReason: None,
purgeLoading: false,
showConfirmLeaveModTeam: false,
};
@ -403,12 +410,19 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{i18n.t("restore")}
</button>
)}
<button
class="btn btn-link text-muted d-inline-block"
onClick={linkEvent(this, this.handlePurgeCommunityShow)}
aria-label={i18n.t("purge_community")}
>
{i18n.t("purge_community")}
</button>
</li>
)}
</ul>
{this.state.showRemoveDialog && (
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<div class="form-group row">
<div class="form-group">
<label class="col-form-label" htmlFor="remove-reason">
{i18n.t("reason")}
</label>
@ -426,13 +440,46 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{/* <label class="col-form-label">Expires</label> */}
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
{/* </div> */}
<div class="form-group row">
<div class="form-group">
<button type="submit" class="btn btn-secondary">
{i18n.t("remove_community")}
</button>
</div>
</form>
)}
{this.state.showPurgeDialog && (
<form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
<div class="form-group">
<PurgeWarning />
</div>
<div class="form-group">
<label class="sr-only" htmlFor="purge-reason">
{i18n.t("reason")}
</label>
<input
type="text"
id="purge-reason"
class="form-control mr-2"
placeholder={i18n.t("reason")}
value={toUndefined(this.state.purgeReason)}
onInput={linkEvent(this, this.handlePurgeReasonChange)}
/>
</div>
<div class="form-group">
{this.state.purgeLoading ? (
<Spinner />
) : (
<button
type="submit"
class="btn btn-secondary"
aria-label={i18n.t("purge_community")}
>
{i18n.t("purge_community")}
</button>
)}
</div>
</form>
)}
</>
);
}
@ -542,13 +589,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
}
handleModRemoveReasonChange(i: Sidebar, event: any) {
i.state.removeReason = event.target.value;
i.state.removeReason = Some(event.target.value);
i.setState(i.state);
}
handleModRemoveExpiresChange(i: Sidebar, event: any) {
console.log(event.target.value);
i.state.removeExpires = event.target.value;
i.state.removeExpires = Some(event.target.value);
i.setState(i.state);
}
@ -566,4 +612,29 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handlePurgeCommunityShow(i: Sidebar) {
i.state.showPurgeDialog = true;
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handlePurgeReasonChange(i: Sidebar, event: any) {
i.state.purgeReason = Some(event.target.value);
i.setState(i.state);
}
handlePurgeSubmit(i: Sidebar, event: any) {
event.preventDefault();
let form = new PurgeCommunity({
community_id: i.props.community_view.community.id,
reason: i.state.purgeReason,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.purgeCommunity(form));
i.state.purgeLoading = true;
i.setState(i.state);
}
}

View file

@ -21,6 +21,7 @@ import {
PostReportResponse,
PostResponse,
PostView,
PurgeItemResponse,
SiteResponse,
SortType,
UserOperation,
@ -860,6 +861,17 @@ export class Home extends Component<any, HomeState> {
if (data) {
toast(i18n.t("report_created"));
}
} else if (
op == UserOperation.PurgePerson ||
op == UserOperation.PurgePost ||
op == UserOperation.PurgeComment ||
op == UserOperation.PurgeCommunity
) {
let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
if (data.success) {
toast(i18n.t("purge_success"));
this.context.router.history.push(`/`);
}
}
}
}

View file

@ -2,6 +2,10 @@ import { None, Option, Some } from "@sniptt/monads";
import { Component } from "inferno";
import { Link } from "inferno-router";
import {
AdminPurgeCommentView,
AdminPurgeCommunityView,
AdminPurgePersonView,
AdminPurgePostView,
CommunityModeratorView,
GetCommunity,
GetCommunityResponse,
@ -57,11 +61,16 @@ enum ModlogEnum {
ModTransferCommunity,
ModAdd,
ModBan,
AdminPurgePerson,
AdminPurgeCommunity,
AdminPurgePost,
AdminPurgeComment,
}
type ModlogType = {
id: number;
type_: ModlogEnum;
moderator: PersonSafe;
view:
| ModRemovePostView
| ModLockPostView
@ -72,7 +81,11 @@ type ModlogType = {
| ModBanView
| ModAddCommunityView
| ModTransferCommunityView
| ModAddView;
| ModAddView
| AdminPurgePersonView
| AdminPurgeCommunityView
| AdminPurgePostView
| AdminPurgeCommentView;
when_: string;
};
@ -118,11 +131,13 @@ export class Modlog extends Component<any, ModlogState> {
if (this.isoData.path == this.context.router.route.match.url) {
this.state.res = Some(this.isoData.routeData[0] as GetModlogResponse);
// Getting the moderators
let communityRes = Some(
this.isoData.routeData[1] as GetCommunityResponse
);
this.state.communityMods = communityRes.map(c => c.moderators);
if (this.isoData.routeData[1]) {
// Getting the moderators
let communityRes = Some(
this.isoData.routeData[1] as GetCommunityResponse
);
this.state.communityMods = communityRes.map(c => c.moderators);
}
this.state.loading = false;
} else {
@ -141,6 +156,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_remove_post.id,
type_: ModlogEnum.ModRemovePost,
view: r,
moderator: r.moderator,
when_: r.mod_remove_post.when_,
}));
@ -148,6 +164,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_lock_post.id,
type_: ModlogEnum.ModLockPost,
view: r,
moderator: r.moderator,
when_: r.mod_lock_post.when_,
}));
@ -155,6 +172,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_sticky_post.id,
type_: ModlogEnum.ModStickyPost,
view: r,
moderator: r.moderator,
when_: r.mod_sticky_post.when_,
}));
@ -162,6 +180,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_remove_comment.id,
type_: ModlogEnum.ModRemoveComment,
view: r,
moderator: r.moderator,
when_: r.mod_remove_comment.when_,
}));
@ -169,6 +188,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_remove_community.id,
type_: ModlogEnum.ModRemoveCommunity,
view: r,
moderator: r.moderator,
when_: r.mod_remove_community.when_,
}));
@ -177,6 +197,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_ban_from_community.id,
type_: ModlogEnum.ModBanFromCommunity,
view: r,
moderator: r.moderator,
when_: r.mod_ban_from_community.when_,
})
);
@ -185,6 +206,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_add_community.id,
type_: ModlogEnum.ModAddCommunity,
view: r,
moderator: r.moderator,
when_: r.mod_add_community.when_,
}));
@ -193,6 +215,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_transfer_community.id,
type_: ModlogEnum.ModTransferCommunity,
view: r,
moderator: r.moderator,
when_: r.mod_transfer_community.when_,
}));
@ -200,6 +223,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_add.id,
type_: ModlogEnum.ModAdd,
view: r,
moderator: r.moderator,
when_: r.mod_add.when_,
}));
@ -207,9 +231,44 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_ban.id,
type_: ModlogEnum.ModBan,
view: r,
moderator: r.moderator,
when_: r.mod_ban.when_,
}));
let purged_persons: ModlogType[] = res.admin_purged_persons.map(r => ({
id: r.admin_purge_person.id,
type_: ModlogEnum.AdminPurgePerson,
view: r,
moderator: r.admin,
when_: r.admin_purge_person.when_,
}));
let purged_communities: ModlogType[] = res.admin_purged_communities.map(
r => ({
id: r.admin_purge_community.id,
type_: ModlogEnum.AdminPurgeCommunity,
view: r,
moderator: r.admin,
when_: r.admin_purge_community.when_,
})
);
let purged_posts: ModlogType[] = res.admin_purged_posts.map(r => ({
id: r.admin_purge_post.id,
type_: ModlogEnum.AdminPurgePost,
view: r,
moderator: r.admin,
when_: r.admin_purge_post.when_,
}));
let purged_comments: ModlogType[] = res.admin_purged_comments.map(r => ({
id: r.admin_purge_comment.id,
type_: ModlogEnum.AdminPurgeComment,
view: r,
moderator: r.admin,
when_: r.admin_purge_comment.when_,
}));
let combined: ModlogType[] = [];
combined.push(...removed_posts);
@ -222,6 +281,10 @@ export class Modlog extends Component<any, ModlogState> {
combined.push(...transferred_to_community);
combined.push(...added);
combined.push(...banned);
combined.push(...purged_persons);
combined.push(...purged_communities);
combined.push(...purged_posts);
combined.push(...purged_comments);
// Sort them by time
combined.sort((a, b) => b.when_.localeCompare(a.when_));
@ -234,18 +297,22 @@ export class Modlog extends Component<any, ModlogState> {
case ModlogEnum.ModRemovePost: {
let mrpv = i.view as ModRemovePostView;
return [
mrpv.mod_remove_post.removed ? "Removed " : "Restored ",
mrpv.mod_remove_post.removed.unwrapOr(false)
? "Removed "
: "Restored ",
<span>
Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
</span>,
mrpv.mod_remove_post.reason &&
` reason: ${mrpv.mod_remove_post.reason}`,
mrpv.mod_remove_post.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
];
}
case ModlogEnum.ModLockPost: {
let mlpv = i.view as ModLockPostView;
return [
mlpv.mod_lock_post.locked ? "Locked " : "Unlocked ",
mlpv.mod_lock_post.locked.unwrapOr(false) ? "Locked " : "Unlocked ",
<span>
Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
</span>,
@ -254,7 +321,9 @@ export class Modlog extends Component<any, ModlogState> {
case ModlogEnum.ModStickyPost: {
let mspv = i.view as ModStickyPostView;
return [
mspv.mod_sticky_post.stickied ? "Stickied " : "Unstickied ",
mspv.mod_sticky_post.stickied.unwrapOr(false)
? "Stickied "
: "Unstickied ",
<span>
Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link>
</span>,
@ -263,7 +332,9 @@ export class Modlog extends Component<any, ModlogState> {
case ModlogEnum.ModRemoveComment: {
let mrc = i.view as ModRemoveCommentView;
return [
mrc.mod_remove_comment.removed ? "Removed " : "Restored ",
mrc.mod_remove_comment.removed.unwrapOr(false)
? "Removed "
: "Restored ",
<span>
Comment{" "}
<Link to={`/post/${mrc.post.id}/comment/${mrc.comment.id}`}>
@ -274,30 +345,40 @@ export class Modlog extends Component<any, ModlogState> {
{" "}
by <PersonListing person={mrc.commenter} />
</span>,
mrc.mod_remove_comment.reason &&
` reason: ${mrc.mod_remove_comment.reason}`,
mrc.mod_remove_comment.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
];
}
case ModlogEnum.ModRemoveCommunity: {
let mrco = i.view as ModRemoveCommunityView;
return [
mrco.mod_remove_community.removed ? "Removed " : "Restored ",
mrco.mod_remove_community.removed.unwrapOr(false)
? "Removed "
: "Restored ",
<span>
Community <CommunityLink community={mrco.community} />
</span>,
mrco.mod_remove_community.reason.isSome() &&
` reason: ${mrco.mod_remove_community.reason.unwrap()}`,
mrco.mod_remove_community.expires.isSome() &&
` expires: ${moment
.utc(mrco.mod_remove_community.expires.unwrap())
.fromNow()}`,
mrco.mod_remove_community.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
mrco.mod_remove_community.expires.match({
some: expires => (
<div>expires: {moment.utc(expires).fromNow()}</div>
),
none: <></>,
}),
];
}
case ModlogEnum.ModBanFromCommunity: {
let mbfc = i.view as ModBanFromCommunityView;
return [
<span>
{mbfc.mod_ban_from_community.banned ? "Banned " : "Unbanned "}{" "}
{mbfc.mod_ban_from_community.banned.unwrapOr(false)
? "Banned "
: "Unbanned "}{" "}
</span>,
<span>
<PersonListing person={mbfc.banned_person} />
@ -306,23 +387,25 @@ export class Modlog extends Component<any, ModlogState> {
<span>
<CommunityLink community={mbfc.community} />
</span>,
<div>
{mbfc.mod_ban_from_community.reason.isSome() &&
` reason: ${mbfc.mod_ban_from_community.reason.unwrap()}`}
</div>,
<div>
{mbfc.mod_ban_from_community.expires.isSome() &&
` expires: ${moment
.utc(mbfc.mod_ban_from_community.expires.unwrap())
.fromNow()}`}
</div>,
mbfc.mod_ban_from_community.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
mbfc.mod_ban_from_community.expires.match({
some: expires => (
<div>expires: {moment.utc(expires).fromNow()}</div>
),
none: <></>,
}),
];
}
case ModlogEnum.ModAddCommunity: {
let mac = i.view as ModAddCommunityView;
return [
<span>
{mac.mod_add_community.removed ? "Removed " : "Appointed "}{" "}
{mac.mod_add_community.removed.unwrapOr(false)
? "Removed "
: "Appointed "}{" "}
</span>,
<span>
<PersonListing person={mac.modded_person} />
@ -337,7 +420,9 @@ export class Modlog extends Component<any, ModlogState> {
let mtc = i.view as ModTransferCommunityView;
return [
<span>
{mtc.mod_transfer_community.removed ? "Removed " : "Transferred "}{" "}
{mtc.mod_transfer_community.removed.unwrapOr(false)
? "Removed "
: "Transferred "}{" "}
</span>,
<span>
<CommunityLink community={mtc.community} />
@ -351,27 +436,29 @@ export class Modlog extends Component<any, ModlogState> {
case ModlogEnum.ModBan: {
let mb = i.view as ModBanView;
return [
<span>{mb.mod_ban.banned ? "Banned " : "Unbanned "} </span>,
<span>
{mb.mod_ban.banned.unwrapOr(false) ? "Banned " : "Unbanned "}{" "}
</span>,
<span>
<PersonListing person={mb.banned_person} />
</span>,
<div>
{mb.mod_ban.reason.isSome() &&
` reason: ${mb.mod_ban.reason.unwrap()}`}
</div>,
<div>
{mb.mod_ban.expires.isSome() &&
` expires: ${moment.utc(mb.mod_ban.expires.unwrap()).fromNow()}`}
</div>,
mb.mod_ban.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
mb.mod_ban.expires.match({
some: expires => (
<div>expires: {moment.utc(expires).fromNow()}</div>
),
none: <></>,
}),
];
}
case ModlogEnum.ModAdd: {
let ma = i.view as ModAddView;
return [
<span>
{ma.mod_add.removed.isSome() && ma.mod_add.removed.unwrap()
? "Removed "
: "Appointed "}{" "}
{ma.mod_add.removed.unwrapOr(false) ? "Removed " : "Appointed "}{" "}
</span>,
<span>
<PersonListing person={ma.modded_person} />
@ -379,6 +466,50 @@ export class Modlog extends Component<any, ModlogState> {
<span> as an admin </span>,
];
}
case ModlogEnum.AdminPurgePerson: {
let ap = i.view as AdminPurgePersonView;
return [
<span>Purged a Person</span>,
ap.admin_purge_person.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
];
}
case ModlogEnum.AdminPurgeCommunity: {
let ap = i.view as AdminPurgeCommunityView;
return [
<span>Purged a Community</span>,
ap.admin_purge_community.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
];
}
case ModlogEnum.AdminPurgePost: {
let ap = i.view as AdminPurgePostView;
return [
<span>Purged a Post from from </span>,
<CommunityLink community={ap.community} />,
ap.admin_purge_post.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
];
}
case ModlogEnum.AdminPurgeComment: {
let ap = i.view as AdminPurgeCommentView;
return [
<span>
Purged a Comment from{" "}
<Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link>
</span>,
ap.admin_purge_comment.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
];
}
default:
return <div />;
}
@ -396,9 +527,9 @@ export class Modlog extends Component<any, ModlogState> {
</td>
<td>
{this.amAdminOrMod ? (
<PersonListing person={i.view.moderator} />
<PersonListing person={i.moderator} />
) : (
<div>{this.modOrAdminText(i.view.moderator)}</div>
<div>{this.modOrAdminText(i.moderator)}</div>
)}
</td>
<td>{this.renderModlogType(i)}</td>
@ -415,7 +546,7 @@ export class Modlog extends Component<any, ModlogState> {
);
}
modOrAdminText(person: PersonSafe): Text {
modOrAdminText(person: PersonSafe): string {
if (
this.isoData.site_res.admins.map(a => a.person.id).includes(person.id)
) {

View file

@ -12,6 +12,7 @@ import {
GetPersonDetailsResponse,
GetSiteResponse,
PostResponse,
PurgeItemResponse,
SortType,
toUndefined,
UserOperation,
@ -897,6 +898,17 @@ export class Profile extends Component<any, ProfileState> {
updatePersonBlock(data);
this.setPersonBlock();
this.setState(this.state);
} else if (
op == UserOperation.PurgePerson ||
op == UserOperation.PurgePost ||
op == UserOperation.PurgeComment ||
op == UserOperation.PurgeCommunity
) {
let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
if (data.success) {
toast(i18n.t("purge_success"));
this.context.router.history.push(`/`);
}
}
}
}

View file

@ -15,6 +15,8 @@ import {
LockPost,
PersonViewSafe,
PostView,
PurgePerson,
PurgePost,
RemovePost,
SavePost,
StickyPost,
@ -23,7 +25,7 @@ import {
} from "lemmy-js-client";
import { externalHost } from "../../env";
import { i18n } from "../../i18next";
import { BanType } from "../../interfaces";
import { BanType, PurgeType } from "../../interfaces";
import { UserService, WebSocketService } from "../../services";
import {
amCommunityCreator,
@ -45,7 +47,7 @@ import {
showScores,
wsClient,
} from "../../utils";
import { Icon } from "../common/icon";
import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time";
import { PictrsImage } from "../common/pictrs-image";
import { CommunityLink } from "../community/community-link";
@ -56,6 +58,10 @@ import { PostForm } from "./post-form";
interface PostListingState {
showEdit: boolean;
showRemoveDialog: boolean;
showPurgeDialog: boolean;
purgeReason: Option<string>;
purgeType: PurgeType;
purgeLoading: boolean;
removeReason: Option<string>;
showBanDialog: boolean;
banReason: Option<string>;
@ -93,6 +99,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
private emptyState: PostListingState = {
showEdit: false,
showRemoveDialog: false,
showPurgeDialog: false,
purgeReason: None,
purgeType: PurgeType.Person,
purgeLoading: false,
removeReason: None,
showBanDialog: false,
banReason: None,
@ -943,24 +953,41 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{/* Admins can ban from all, and appoint other admins */}
{this.canAdmin_ && (
<>
{!this.creatorIsAdmin_ &&
(!isBanned(post_view.creator) ? (
{!this.creatorIsAdmin_ && (
<>
{!isBanned(post_view.creator) ? (
<button
class="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleModBanShow)}
aria-label={i18n.t("ban_from_site")}
>
{i18n.t("ban_from_site")}
</button>
) : (
<button
class="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleModBanSubmit)}
aria-label={i18n.t("unban_from_site")}
>
{i18n.t("unban_from_site")}
</button>
)}
<button
class="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleModBanShow)}
aria-label={i18n.t("ban_from_site")}
onClick={linkEvent(this, this.handlePurgePersonShow)}
aria-label={i18n.t("purge_user")}
>
{i18n.t("ban_from_site")}
{i18n.t("purge_user")}
</button>
) : (
<button
class="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleModBanSubmit)}
aria-label={i18n.t("unban_from_site")}
onClick={linkEvent(this, this.handlePurgePostShow)}
aria-label={i18n.t("purge_post")}
>
{i18n.t("unban_from_site")}
{i18n.t("purge_post")}
</button>
))}
</>
)}
{!isBanned(post_view.creator) && post_view.creator.local && (
<button
class="btn btn-link btn-animate text-muted py-0"
@ -985,6 +1012,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
removeAndBanDialogs() {
let post = this.props.post_view;
let purgeTypeText: string;
if (this.state.purgeType == PurgeType.Post) {
purgeTypeText = i18n.t("purge_post");
} else if (this.state.purgeType == PurgeType.Person) {
purgeTypeText = `${i18n.t("purge")} ${post.creator.name}`;
}
return (
<>
{this.state.showRemoveDialog && (
@ -1098,6 +1131,36 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</button>
</form>
)}
{this.state.showPurgeDialog && (
<form
class="form-inline"
onSubmit={linkEvent(this, this.handlePurgeSubmit)}
>
<PurgeWarning />
<label class="sr-only" htmlFor="purge-reason">
{i18n.t("reason")}
</label>
<input
type="text"
id="purge-reason"
class="form-control mr-2"
placeholder={i18n.t("reason")}
value={toUndefined(this.state.purgeReason)}
onInput={linkEvent(this, this.handlePurgeReasonChange)}
/>
{this.state.purgeLoading ? (
<Spinner />
) : (
<button
type="submit"
class="btn btn-secondary"
aria-label={purgeTypeText}
>
{purgeTypeText}
</button>
)}
</form>
)}
</>
);
}
@ -1411,6 +1474,48 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
i.setState(i.state);
}
handlePurgePersonShow(i: PostListing) {
i.state.showPurgeDialog = true;
i.state.purgeType = PurgeType.Person;
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handlePurgePostShow(i: PostListing) {
i.state.showPurgeDialog = true;
i.state.purgeType = PurgeType.Post;
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handlePurgeReasonChange(i: PostListing, event: any) {
i.state.purgeReason = Some(event.target.value);
i.setState(i.state);
}
handlePurgeSubmit(i: PostListing, event: any) {
event.preventDefault();
if (i.state.purgeType == PurgeType.Person) {
let form = new PurgePerson({
person_id: i.props.post_view.creator.id,
reason: i.state.purgeReason,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.purgePerson(form));
} else if (i.state.purgeType == PurgeType.Post) {
let form = new PurgePost({
post_id: i.props.post_view.post.id,
reason: i.state.purgeReason,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.purgePost(form));
}
i.state.purgeLoading = true;
i.setState(i.state);
}
handleModBanReasonChange(i: PostListing, event: any) {
i.state.banReason = Some(event.target.value);
i.setState(i.state);

View file

@ -19,6 +19,7 @@ import {
PostReportResponse,
PostResponse,
PostView,
PurgeItemResponse,
Search,
SearchResponse,
SearchType,
@ -760,6 +761,17 @@ export class Post extends Component<any, PostState> {
if (data) {
toast(i18n.t("report_created"));
}
} else if (
op == UserOperation.PurgePerson ||
op == UserOperation.PurgePost ||
op == UserOperation.PurgeComment ||
op == UserOperation.PurgeCommunity
) {
let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
if (data.success) {
toast(i18n.t("purge_success"));
this.context.router.history.push(`/`);
}
}
}
}

View file

@ -73,3 +73,10 @@ export enum PersonDetailsView {
Posts,
Saved,
}
export enum PurgeType {
Person,
Community,
Post,
Comment,
}

View file

@ -4948,10 +4948,10 @@ lcid@^1.0.0:
dependencies:
invert-kv "^1.0.0"
lemmy-js-client@0.17.0-rc.32:
version "0.17.0-rc.32"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.32.tgz#d67f432f1fffc54c267f915278fe260ec554b018"
integrity sha512-qPLybaesu3GVr1DMStsyCYanW4maxHrqX71UHadFMeuh+aUK8taC3zfsLRK9dlIlSDRS283xd8IZkI6ZlcOVEQ==
lemmy-js-client@0.17.0-rc.33:
version "0.17.0-rc.33"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.33.tgz#e05cc88213da3c0c21c7ea53c29054041d619150"
integrity sha512-rG0yCc9AAc5/B+muDfWB7bKizBG7r/xSzHeEw5ms50xF4dN+KOqvRcHTf0+15uAYehBF5B54nyxdlKPRKL9GxQ==
levn@^0.4.1:
version "0.4.1"