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", "eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.4", "husky": "^7.0.4",
"import-sort-style-module": "^6.0.0", "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", "lint-staged": "^12.4.1",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.6.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",

View file

@ -17,6 +17,8 @@ import {
MarkPersonMentionAsRead, MarkPersonMentionAsRead,
PersonMentionView, PersonMentionView,
PersonViewSafe, PersonViewSafe,
PurgeComment,
PurgePerson,
RemoveComment, RemoveComment,
SaveComment, SaveComment,
toUndefined, toUndefined,
@ -24,7 +26,11 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import moment from "moment"; import moment from "moment";
import { i18n } from "../../i18next"; 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 { UserService, WebSocketService } from "../../services";
import { import {
amCommunityCreator, amCommunityCreator,
@ -42,7 +48,7 @@ import {
showScores, showScores,
wsClient, wsClient,
} from "../../utils"; } from "../../utils";
import { Icon, Spinner } from "../common/icon"; import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time"; import { MomentTime } from "../common/moment-time";
import { CommunityLink } from "../community/community-link"; import { CommunityLink } from "../community/community-link";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
@ -59,6 +65,10 @@ interface CommentNodeState {
banReason: Option<string>; banReason: Option<string>;
banExpireDays: Option<number>; banExpireDays: Option<number>;
banType: BanType; banType: BanType;
showPurgeDialog: boolean;
purgeReason: Option<string>;
purgeType: PurgeType;
purgeLoading: boolean;
showConfirmTransferSite: boolean; showConfirmTransferSite: boolean;
showConfirmTransferCommunity: boolean; showConfirmTransferCommunity: boolean;
showConfirmAppointAsMod: boolean; showConfirmAppointAsMod: boolean;
@ -102,6 +112,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
banReason: None, banReason: None,
banExpireDays: None, banExpireDays: None,
banType: BanType.Community, banType: BanType.Community,
showPurgeDialog: false,
purgeLoading: false,
purgeReason: None,
purgeType: PurgeType.Person,
collapsed: false, collapsed: false,
viewSource: false, viewSource: false,
showAdvanced: false, showAdvanced: false,
@ -147,6 +161,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let node = this.props.node; let node = this.props.node;
let cv = this.props.node.comment_view; 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( let canMod_ = canMod(
this.props.moderators, this.props.moderators,
this.props.admins, this.props.admins,
@ -645,8 +666,30 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{/* Admins can ban from all, and appoint other admins */} {/* Admins can ban from all, and appoint other admins */}
{canAdmin_ && ( {canAdmin_ && (
<> <>
{!isAdmin_ && {!isAdmin_ && (
(!isBanned(cv.creator) ? ( <>
<button
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
this.handlePurgePersonShow
)}
aria-label={i18n.t("purge_user")}
>
{i18n.t("purge_user")}
</button>
<button
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
this.handlePurgeCommentShow
)}
aria-label={i18n.t("purge_comment")}
>
{i18n.t("purge_comment")}
</button>
{!isBanned(cv.creator) ? (
<button <button
class="btn btn-link btn-animate text-muted" class="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
@ -668,7 +711,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
> >
{i18n.t("unban_from_site")} {i18n.t("unban_from_site")}
</button> </button>
))} )}
</>
)}
{!isBanned(cv.creator) && {!isBanned(cv.creator) &&
cv.creator.local && cv.creator.local &&
(!this.state.showConfirmAppointAsAdmin ? ( (!this.state.showConfirmAppointAsAdmin ? (
@ -848,6 +893,36 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</div> </div>
</form> </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 && ( {this.state.showReply && (
<CommentForm <CommentForm
node={Left(node)} node={Left(node)}
@ -1202,6 +1277,48 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.setState(i.state); 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) { handleShowConfirmAppointAsMod(i: CommentNode) {
i.state.showConfirmAppointAsMod = true; i.state.showConfirmAppointAsMod = true;
i.setState(i.state); i.setState(i.state);

View file

@ -1,5 +1,6 @@
import classNames from "classnames"; import classNames from "classnames";
import { Component } from "inferno"; import { Component } from "inferno";
import { i18n } from "../../i18next";
interface IconProps { interface IconProps {
icon: string; 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, PostReportResponse,
PostResponse, PostResponse,
PostView, PostView,
PurgeItemResponse,
SortType, SortType,
toOption, toOption,
UserOperation, UserOperation,
@ -656,6 +657,12 @@ export class Community extends Component<any, State> {
if (data) { if (data) {
toast(i18n.t("report_created")); 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 { Component, linkEvent } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { import {
@ -8,6 +8,7 @@ import {
DeleteCommunity, DeleteCommunity,
FollowCommunity, FollowCommunity,
PersonViewSafe, PersonViewSafe,
PurgeCommunity,
RemoveCommunity, RemoveCommunity,
SubscribedType, SubscribedType,
toUndefined, toUndefined,
@ -25,7 +26,7 @@ import {
wsClient, wsClient,
} from "../../utils"; } from "../../utils";
import { BannerIconHeader } from "../common/banner-icon-header"; 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 { CommunityForm } from "../community/community-form";
import { CommunityLink } from "../community/community-link"; import { CommunityLink } from "../community/community-link";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
@ -44,6 +45,9 @@ interface SidebarState {
removeExpires: Option<string>; removeExpires: Option<string>;
showEdit: boolean; showEdit: boolean;
showRemoveDialog: boolean; showRemoveDialog: boolean;
showPurgeDialog: boolean;
purgeReason: Option<string>;
purgeLoading: boolean;
showConfirmLeaveModTeam: boolean; showConfirmLeaveModTeam: boolean;
} }
@ -51,8 +55,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
private emptyState: SidebarState = { private emptyState: SidebarState = {
showEdit: false, showEdit: false,
showRemoveDialog: false, showRemoveDialog: false,
removeReason: null, removeReason: None,
removeExpires: null, removeExpires: None,
showPurgeDialog: false,
purgeReason: None,
purgeLoading: false,
showConfirmLeaveModTeam: false, showConfirmLeaveModTeam: false,
}; };
@ -403,12 +410,19 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{i18n.t("restore")} {i18n.t("restore")}
</button> </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> </li>
)} )}
</ul> </ul>
{this.state.showRemoveDialog && ( {this.state.showRemoveDialog && (
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}> <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<div class="form-group row"> <div class="form-group">
<label class="col-form-label" htmlFor="remove-reason"> <label class="col-form-label" htmlFor="remove-reason">
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
@ -426,13 +440,46 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{/* <label class="col-form-label">Expires</label> */} {/* <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)} /> */} {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
{/* </div> */} {/* </div> */}
<div class="form-group row"> <div class="form-group">
<button type="submit" class="btn btn-secondary"> <button type="submit" class="btn btn-secondary">
{i18n.t("remove_community")} {i18n.t("remove_community")}
</button> </button>
</div> </div>
</form> </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) { handleModRemoveReasonChange(i: Sidebar, event: any) {
i.state.removeReason = event.target.value; i.state.removeReason = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
} }
handleModRemoveExpiresChange(i: Sidebar, event: any) { handleModRemoveExpiresChange(i: Sidebar, event: any) {
console.log(event.target.value); i.state.removeExpires = Some(event.target.value);
i.state.removeExpires = event.target.value;
i.setState(i.state); i.setState(i.state);
} }
@ -566,4 +612,29 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
i.state.showRemoveDialog = false; i.state.showRemoveDialog = false;
i.setState(i.state); 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, PostReportResponse,
PostResponse, PostResponse,
PostView, PostView,
PurgeItemResponse,
SiteResponse, SiteResponse,
SortType, SortType,
UserOperation, UserOperation,
@ -860,6 +861,17 @@ export class Home extends Component<any, HomeState> {
if (data) { if (data) {
toast(i18n.t("report_created")); 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 { Component } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { import {
AdminPurgeCommentView,
AdminPurgeCommunityView,
AdminPurgePersonView,
AdminPurgePostView,
CommunityModeratorView, CommunityModeratorView,
GetCommunity, GetCommunity,
GetCommunityResponse, GetCommunityResponse,
@ -57,11 +61,16 @@ enum ModlogEnum {
ModTransferCommunity, ModTransferCommunity,
ModAdd, ModAdd,
ModBan, ModBan,
AdminPurgePerson,
AdminPurgeCommunity,
AdminPurgePost,
AdminPurgeComment,
} }
type ModlogType = { type ModlogType = {
id: number; id: number;
type_: ModlogEnum; type_: ModlogEnum;
moderator: PersonSafe;
view: view:
| ModRemovePostView | ModRemovePostView
| ModLockPostView | ModLockPostView
@ -72,7 +81,11 @@ type ModlogType = {
| ModBanView | ModBanView
| ModAddCommunityView | ModAddCommunityView
| ModTransferCommunityView | ModTransferCommunityView
| ModAddView; | ModAddView
| AdminPurgePersonView
| AdminPurgeCommunityView
| AdminPurgePostView
| AdminPurgeCommentView;
when_: string; when_: string;
}; };
@ -118,11 +131,13 @@ export class Modlog extends Component<any, ModlogState> {
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.res = Some(this.isoData.routeData[0] as GetModlogResponse); this.state.res = Some(this.isoData.routeData[0] as GetModlogResponse);
if (this.isoData.routeData[1]) {
// Getting the moderators // Getting the moderators
let communityRes = Some( let communityRes = Some(
this.isoData.routeData[1] as GetCommunityResponse this.isoData.routeData[1] as GetCommunityResponse
); );
this.state.communityMods = communityRes.map(c => c.moderators); this.state.communityMods = communityRes.map(c => c.moderators);
}
this.state.loading = false; this.state.loading = false;
} else { } else {
@ -141,6 +156,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_remove_post.id, id: r.mod_remove_post.id,
type_: ModlogEnum.ModRemovePost, type_: ModlogEnum.ModRemovePost,
view: r, view: r,
moderator: r.moderator,
when_: r.mod_remove_post.when_, when_: r.mod_remove_post.when_,
})); }));
@ -148,6 +164,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_lock_post.id, id: r.mod_lock_post.id,
type_: ModlogEnum.ModLockPost, type_: ModlogEnum.ModLockPost,
view: r, view: r,
moderator: r.moderator,
when_: r.mod_lock_post.when_, when_: r.mod_lock_post.when_,
})); }));
@ -155,6 +172,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_sticky_post.id, id: r.mod_sticky_post.id,
type_: ModlogEnum.ModStickyPost, type_: ModlogEnum.ModStickyPost,
view: r, view: r,
moderator: r.moderator,
when_: r.mod_sticky_post.when_, when_: r.mod_sticky_post.when_,
})); }));
@ -162,6 +180,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_remove_comment.id, id: r.mod_remove_comment.id,
type_: ModlogEnum.ModRemoveComment, type_: ModlogEnum.ModRemoveComment,
view: r, view: r,
moderator: r.moderator,
when_: r.mod_remove_comment.when_, when_: r.mod_remove_comment.when_,
})); }));
@ -169,6 +188,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_remove_community.id, id: r.mod_remove_community.id,
type_: ModlogEnum.ModRemoveCommunity, type_: ModlogEnum.ModRemoveCommunity,
view: r, view: r,
moderator: r.moderator,
when_: r.mod_remove_community.when_, when_: r.mod_remove_community.when_,
})); }));
@ -177,6 +197,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_ban_from_community.id, id: r.mod_ban_from_community.id,
type_: ModlogEnum.ModBanFromCommunity, type_: ModlogEnum.ModBanFromCommunity,
view: r, view: r,
moderator: r.moderator,
when_: r.mod_ban_from_community.when_, when_: r.mod_ban_from_community.when_,
}) })
); );
@ -185,6 +206,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_add_community.id, id: r.mod_add_community.id,
type_: ModlogEnum.ModAddCommunity, type_: ModlogEnum.ModAddCommunity,
view: r, view: r,
moderator: r.moderator,
when_: r.mod_add_community.when_, when_: r.mod_add_community.when_,
})); }));
@ -193,6 +215,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_transfer_community.id, id: r.mod_transfer_community.id,
type_: ModlogEnum.ModTransferCommunity, type_: ModlogEnum.ModTransferCommunity,
view: r, view: r,
moderator: r.moderator,
when_: r.mod_transfer_community.when_, when_: r.mod_transfer_community.when_,
})); }));
@ -200,6 +223,7 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_add.id, id: r.mod_add.id,
type_: ModlogEnum.ModAdd, type_: ModlogEnum.ModAdd,
view: r, view: r,
moderator: r.moderator,
when_: r.mod_add.when_, when_: r.mod_add.when_,
})); }));
@ -207,9 +231,44 @@ export class Modlog extends Component<any, ModlogState> {
id: r.mod_ban.id, id: r.mod_ban.id,
type_: ModlogEnum.ModBan, type_: ModlogEnum.ModBan,
view: r, view: r,
moderator: r.moderator,
when_: r.mod_ban.when_, 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[] = []; let combined: ModlogType[] = [];
combined.push(...removed_posts); combined.push(...removed_posts);
@ -222,6 +281,10 @@ export class Modlog extends Component<any, ModlogState> {
combined.push(...transferred_to_community); combined.push(...transferred_to_community);
combined.push(...added); combined.push(...added);
combined.push(...banned); combined.push(...banned);
combined.push(...purged_persons);
combined.push(...purged_communities);
combined.push(...purged_posts);
combined.push(...purged_comments);
// Sort them by time // Sort them by time
combined.sort((a, b) => b.when_.localeCompare(a.when_)); combined.sort((a, b) => b.when_.localeCompare(a.when_));
@ -234,18 +297,22 @@ export class Modlog extends Component<any, ModlogState> {
case ModlogEnum.ModRemovePost: { case ModlogEnum.ModRemovePost: {
let mrpv = i.view as ModRemovePostView; let mrpv = i.view as ModRemovePostView;
return [ return [
mrpv.mod_remove_post.removed ? "Removed " : "Restored ", mrpv.mod_remove_post.removed.unwrapOr(false)
? "Removed "
: "Restored ",
<span> <span>
Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link> Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
</span>, </span>,
mrpv.mod_remove_post.reason && mrpv.mod_remove_post.reason.match({
` reason: ${mrpv.mod_remove_post.reason}`, some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
]; ];
} }
case ModlogEnum.ModLockPost: { case ModlogEnum.ModLockPost: {
let mlpv = i.view as ModLockPostView; let mlpv = i.view as ModLockPostView;
return [ return [
mlpv.mod_lock_post.locked ? "Locked " : "Unlocked ", mlpv.mod_lock_post.locked.unwrapOr(false) ? "Locked " : "Unlocked ",
<span> <span>
Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link> Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
</span>, </span>,
@ -254,7 +321,9 @@ export class Modlog extends Component<any, ModlogState> {
case ModlogEnum.ModStickyPost: { case ModlogEnum.ModStickyPost: {
let mspv = i.view as ModStickyPostView; let mspv = i.view as ModStickyPostView;
return [ return [
mspv.mod_sticky_post.stickied ? "Stickied " : "Unstickied ", mspv.mod_sticky_post.stickied.unwrapOr(false)
? "Stickied "
: "Unstickied ",
<span> <span>
Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link> Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link>
</span>, </span>,
@ -263,7 +332,9 @@ export class Modlog extends Component<any, ModlogState> {
case ModlogEnum.ModRemoveComment: { case ModlogEnum.ModRemoveComment: {
let mrc = i.view as ModRemoveCommentView; let mrc = i.view as ModRemoveCommentView;
return [ return [
mrc.mod_remove_comment.removed ? "Removed " : "Restored ", mrc.mod_remove_comment.removed.unwrapOr(false)
? "Removed "
: "Restored ",
<span> <span>
Comment{" "} Comment{" "}
<Link to={`/post/${mrc.post.id}/comment/${mrc.comment.id}`}> <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} /> by <PersonListing person={mrc.commenter} />
</span>, </span>,
mrc.mod_remove_comment.reason && mrc.mod_remove_comment.reason.match({
` reason: ${mrc.mod_remove_comment.reason}`, some: reason => <div>reason: {reason}</div>,
none: <></>,
}),
]; ];
} }
case ModlogEnum.ModRemoveCommunity: { case ModlogEnum.ModRemoveCommunity: {
let mrco = i.view as ModRemoveCommunityView; let mrco = i.view as ModRemoveCommunityView;
return [ return [
mrco.mod_remove_community.removed ? "Removed " : "Restored ", mrco.mod_remove_community.removed.unwrapOr(false)
? "Removed "
: "Restored ",
<span> <span>
Community <CommunityLink community={mrco.community} /> Community <CommunityLink community={mrco.community} />
</span>, </span>,
mrco.mod_remove_community.reason.isSome() && mrco.mod_remove_community.reason.match({
` reason: ${mrco.mod_remove_community.reason.unwrap()}`, some: reason => <div>reason: {reason}</div>,
mrco.mod_remove_community.expires.isSome() && none: <></>,
` expires: ${moment }),
.utc(mrco.mod_remove_community.expires.unwrap()) mrco.mod_remove_community.expires.match({
.fromNow()}`, some: expires => (
<div>expires: {moment.utc(expires).fromNow()}</div>
),
none: <></>,
}),
]; ];
} }
case ModlogEnum.ModBanFromCommunity: { case ModlogEnum.ModBanFromCommunity: {
let mbfc = i.view as ModBanFromCommunityView; let mbfc = i.view as ModBanFromCommunityView;
return [ return [
<span> <span>
{mbfc.mod_ban_from_community.banned ? "Banned " : "Unbanned "}{" "} {mbfc.mod_ban_from_community.banned.unwrapOr(false)
? "Banned "
: "Unbanned "}{" "}
</span>, </span>,
<span> <span>
<PersonListing person={mbfc.banned_person} /> <PersonListing person={mbfc.banned_person} />
@ -306,23 +387,25 @@ export class Modlog extends Component<any, ModlogState> {
<span> <span>
<CommunityLink community={mbfc.community} /> <CommunityLink community={mbfc.community} />
</span>, </span>,
<div> mbfc.mod_ban_from_community.reason.match({
{mbfc.mod_ban_from_community.reason.isSome() && some: reason => <div>reason: {reason}</div>,
` reason: ${mbfc.mod_ban_from_community.reason.unwrap()}`} none: <></>,
</div>, }),
<div> mbfc.mod_ban_from_community.expires.match({
{mbfc.mod_ban_from_community.expires.isSome() && some: expires => (
` expires: ${moment <div>expires: {moment.utc(expires).fromNow()}</div>
.utc(mbfc.mod_ban_from_community.expires.unwrap()) ),
.fromNow()}`} none: <></>,
</div>, }),
]; ];
} }
case ModlogEnum.ModAddCommunity: { case ModlogEnum.ModAddCommunity: {
let mac = i.view as ModAddCommunityView; let mac = i.view as ModAddCommunityView;
return [ return [
<span> <span>
{mac.mod_add_community.removed ? "Removed " : "Appointed "}{" "} {mac.mod_add_community.removed.unwrapOr(false)
? "Removed "
: "Appointed "}{" "}
</span>, </span>,
<span> <span>
<PersonListing person={mac.modded_person} /> <PersonListing person={mac.modded_person} />
@ -337,7 +420,9 @@ export class Modlog extends Component<any, ModlogState> {
let mtc = i.view as ModTransferCommunityView; let mtc = i.view as ModTransferCommunityView;
return [ return [
<span> <span>
{mtc.mod_transfer_community.removed ? "Removed " : "Transferred "}{" "} {mtc.mod_transfer_community.removed.unwrapOr(false)
? "Removed "
: "Transferred "}{" "}
</span>, </span>,
<span> <span>
<CommunityLink community={mtc.community} /> <CommunityLink community={mtc.community} />
@ -351,27 +436,29 @@ export class Modlog extends Component<any, ModlogState> {
case ModlogEnum.ModBan: { case ModlogEnum.ModBan: {
let mb = i.view as ModBanView; let mb = i.view as ModBanView;
return [ return [
<span>{mb.mod_ban.banned ? "Banned " : "Unbanned "} </span>, <span>
{mb.mod_ban.banned.unwrapOr(false) ? "Banned " : "Unbanned "}{" "}
</span>,
<span> <span>
<PersonListing person={mb.banned_person} /> <PersonListing person={mb.banned_person} />
</span>, </span>,
<div> mb.mod_ban.reason.match({
{mb.mod_ban.reason.isSome() && some: reason => <div>reason: {reason}</div>,
` reason: ${mb.mod_ban.reason.unwrap()}`} none: <></>,
</div>, }),
<div> mb.mod_ban.expires.match({
{mb.mod_ban.expires.isSome() && some: expires => (
` expires: ${moment.utc(mb.mod_ban.expires.unwrap()).fromNow()}`} <div>expires: {moment.utc(expires).fromNow()}</div>
</div>, ),
none: <></>,
}),
]; ];
} }
case ModlogEnum.ModAdd: { case ModlogEnum.ModAdd: {
let ma = i.view as ModAddView; let ma = i.view as ModAddView;
return [ return [
<span> <span>
{ma.mod_add.removed.isSome() && ma.mod_add.removed.unwrap() {ma.mod_add.removed.unwrapOr(false) ? "Removed " : "Appointed "}{" "}
? "Removed "
: "Appointed "}{" "}
</span>, </span>,
<span> <span>
<PersonListing person={ma.modded_person} /> <PersonListing person={ma.modded_person} />
@ -379,6 +466,50 @@ export class Modlog extends Component<any, ModlogState> {
<span> as an admin </span>, <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: default:
return <div />; return <div />;
} }
@ -396,9 +527,9 @@ export class Modlog extends Component<any, ModlogState> {
</td> </td>
<td> <td>
{this.amAdminOrMod ? ( {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>
<td>{this.renderModlogType(i)}</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 ( if (
this.isoData.site_res.admins.map(a => a.person.id).includes(person.id) this.isoData.site_res.admins.map(a => a.person.id).includes(person.id)
) { ) {

View file

@ -12,6 +12,7 @@ import {
GetPersonDetailsResponse, GetPersonDetailsResponse,
GetSiteResponse, GetSiteResponse,
PostResponse, PostResponse,
PurgeItemResponse,
SortType, SortType,
toUndefined, toUndefined,
UserOperation, UserOperation,
@ -897,6 +898,17 @@ export class Profile extends Component<any, ProfileState> {
updatePersonBlock(data); updatePersonBlock(data);
this.setPersonBlock(); this.setPersonBlock();
this.setState(this.state); 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, LockPost,
PersonViewSafe, PersonViewSafe,
PostView, PostView,
PurgePerson,
PurgePost,
RemovePost, RemovePost,
SavePost, SavePost,
StickyPost, StickyPost,
@ -23,7 +25,7 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import { externalHost } from "../../env"; import { externalHost } from "../../env";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { BanType } from "../../interfaces"; import { BanType, PurgeType } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
amCommunityCreator, amCommunityCreator,
@ -45,7 +47,7 @@ import {
showScores, showScores,
wsClient, wsClient,
} from "../../utils"; } from "../../utils";
import { Icon } from "../common/icon"; import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time"; import { MomentTime } from "../common/moment-time";
import { PictrsImage } from "../common/pictrs-image"; import { PictrsImage } from "../common/pictrs-image";
import { CommunityLink } from "../community/community-link"; import { CommunityLink } from "../community/community-link";
@ -56,6 +58,10 @@ import { PostForm } from "./post-form";
interface PostListingState { interface PostListingState {
showEdit: boolean; showEdit: boolean;
showRemoveDialog: boolean; showRemoveDialog: boolean;
showPurgeDialog: boolean;
purgeReason: Option<string>;
purgeType: PurgeType;
purgeLoading: boolean;
removeReason: Option<string>; removeReason: Option<string>;
showBanDialog: boolean; showBanDialog: boolean;
banReason: Option<string>; banReason: Option<string>;
@ -93,6 +99,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
private emptyState: PostListingState = { private emptyState: PostListingState = {
showEdit: false, showEdit: false,
showRemoveDialog: false, showRemoveDialog: false,
showPurgeDialog: false,
purgeReason: None,
purgeType: PurgeType.Person,
purgeLoading: false,
removeReason: None, removeReason: None,
showBanDialog: false, showBanDialog: false,
banReason: None, banReason: None,
@ -943,8 +953,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{/* Admins can ban from all, and appoint other admins */} {/* Admins can ban from all, and appoint other admins */}
{this.canAdmin_ && ( {this.canAdmin_ && (
<> <>
{!this.creatorIsAdmin_ && {!this.creatorIsAdmin_ && (
(!isBanned(post_view.creator) ? ( <>
{!isBanned(post_view.creator) ? (
<button <button
class="btn btn-link btn-animate text-muted py-0" class="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleModBanShow)} onClick={linkEvent(this, this.handleModBanShow)}
@ -960,7 +971,23 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
> >
{i18n.t("unban_from_site")} {i18n.t("unban_from_site")}
</button> </button>
))} )}
<button
class="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handlePurgePersonShow)}
aria-label={i18n.t("purge_user")}
>
{i18n.t("purge_user")}
</button>
<button
class="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handlePurgePostShow)}
aria-label={i18n.t("purge_post")}
>
{i18n.t("purge_post")}
</button>
</>
)}
{!isBanned(post_view.creator) && post_view.creator.local && ( {!isBanned(post_view.creator) && post_view.creator.local && (
<button <button
class="btn btn-link btn-animate text-muted py-0" class="btn btn-link btn-animate text-muted py-0"
@ -985,6 +1012,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
removeAndBanDialogs() { removeAndBanDialogs() {
let post = this.props.post_view; 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 ( return (
<> <>
{this.state.showRemoveDialog && ( {this.state.showRemoveDialog && (
@ -1098,6 +1131,36 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</button> </button>
</form> </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); 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) { handleModBanReasonChange(i: PostListing, event: any) {
i.state.banReason = Some(event.target.value); i.state.banReason = Some(event.target.value);
i.setState(i.state); i.setState(i.state);

View file

@ -19,6 +19,7 @@ import {
PostReportResponse, PostReportResponse,
PostResponse, PostResponse,
PostView, PostView,
PurgeItemResponse,
Search, Search,
SearchResponse, SearchResponse,
SearchType, SearchType,
@ -760,6 +761,17 @@ export class Post extends Component<any, PostState> {
if (data) { if (data) {
toast(i18n.t("report_created")); 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, Posts,
Saved, Saved,
} }
export enum PurgeType {
Person,
Community,
Post,
Comment,
}

View file

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