Notify users that they are banned from a community (#2397)

* Add banned blurb to community sidebar

* Hide interactable parts of posts and comments when banned from community

* Add translation

* Fix some typescript errors

* Fix typescript errors

* PR feedback
This commit is contained in:
SleeplessOne1917 2024-03-26 19:03:02 -04:00 committed by GitHub
parent 610789242b
commit 579aea40d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 6293 additions and 3069 deletions

@ -1 +1 @@
Subproject commit 62c8418021bc39543c87b4ae3dcf2419d13f61e0 Subproject commit b4c63029e598c022a04fc21acb45855645bd6794

View file

@ -25,33 +25,33 @@
"node": ">=8.9.0" "node": ">=8.9.0"
}, },
"dependencies": { "dependencies": {
"@babel/plugin-proposal-decorators": "^7.23.9", "@babel/plugin-proposal-decorators": "^7.24.1",
"@babel/plugin-transform-class-properties": "^7.23.3", "@babel/plugin-transform-class-properties": "^7.24.1",
"@babel/plugin-transform-runtime": "^7.23.9", "@babel/plugin-transform-runtime": "^7.24.3",
"@babel/plugin-transform-typescript": "^7.23.6", "@babel/plugin-transform-typescript": "^7.24.1",
"@babel/preset-env": "^7.23.9", "@babel/preset-env": "^7.24.3",
"@babel/preset-typescript": "^7.23.3", "@babel/preset-typescript": "^7.24.1",
"@babel/runtime": "^7.23.9", "@babel/runtime": "^7.24.1",
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.1.2",
"@shortcm/qr-image": "^9.0.4", "@shortcm/qr-image": "^9.0.4",
"autosize": "^6.0.1", "autosize": "^6.0.1",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"babel-plugin-inferno": "^6.7.1", "babel-plugin-inferno": "^6.7.1",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"check-password-strength": "^2.0.7", "check-password-strength": "^2.0.10",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"cookie": "^0.6.0", "cookie": "^0.6.0",
"copy-webpack-plugin": "^12.0.2", "copy-webpack-plugin": "^12.0.2",
"css-loader": "^6.10.0", "css-loader": "^6.10.0",
"date-fns": "^3.3.1", "date-fns": "^3.6.0",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.5.2",
"emoji-short-name": "^2.0.0", "emoji-short-name": "^2.0.0",
"express": "~4.18.2", "express": "~4.19.2",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"history": "^5.3.0", "history": "^5.3.0",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"i18next": "^23.10.0", "i18next": "^23.10.1",
"inferno": "^8.2.3", "inferno": "^8.2.3",
"inferno-create-element": "^8.2.3", "inferno-create-element": "^8.2.3",
"inferno-helmet": "^5.2.1", "inferno-helmet": "^5.2.1",
@ -60,9 +60,9 @@
"inferno-router": "^8.2.3", "inferno-router": "^8.2.3",
"inferno-server": "^8.2.3", "inferno-server": "^8.2.3",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lemmy-js-client": "0.19.4-alpha.4", "lemmy-js-client": "0.19.4-alpha.14",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"markdown-it": "^14.0.0", "markdown-it": "^14.1.0",
"markdown-it-bidi": "^0.1.0", "markdown-it-bidi": "^0.1.0",
"markdown-it-container": "^4.0.0", "markdown-it-container": "^4.0.0",
"markdown-it-emoji": "^3.0.0", "markdown-it-emoji": "^3.0.0",
@ -72,25 +72,25 @@
"markdown-it-ruby": "^0.1.1", "markdown-it-ruby": "^0.1.1",
"markdown-it-sub": "^2.0.0", "markdown-it-sub": "^2.0.0",
"markdown-it-sup": "^2.0.0", "markdown-it-sup": "^2.0.0",
"mini-css-extract-plugin": "^2.8.0", "mini-css-extract-plugin": "^2.8.1",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"run-node-webpack-plugin": "^1.3.0", "run-node-webpack-plugin": "^1.3.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sanitize-html": "^2.12.1", "sanitize-html": "^2.13.0",
"sass": "^1.71.1", "sass": "^1.72.0",
"sass-loader": "^14.1.1", "sass-loader": "^14.1.1",
"serialize-javascript": "^6.0.2", "serialize-javascript": "^6.0.2",
"service-worker-webpack": "^1.0.0", "service-worker-webpack": "^1.0.0",
"sharp": "0.33.2", "sharp": "0.33.3",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"toastify-js": "^1.12.0", "toastify-js": "^1.12.0",
"tributejs": "^5.1.3", "tributejs": "^5.1.3",
"webpack": "^5.90.3", "webpack": "^5.91.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0" "webpack-node-externals": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.23.9", "@babel/core": "^7.24.3",
"@types/autosize": "^4.0.3", "@types/autosize": "^4.0.3",
"@types/bootstrap": "^5.2.10", "@types/bootstrap": "^5.2.10",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
@ -99,13 +99,13 @@
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/markdown-it": "^13.0.7", "@types/markdown-it": "^13.0.7",
"@types/markdown-it-container": "^2.0.9", "@types/markdown-it-container": "^2.0.9",
"@types/node": "^20.11.20", "@types/node": "^20.11.30",
"@types/path-browserify": "^1.0.2", "@types/path-browserify": "^1.0.2",
"@types/sanitize-html": "^2.11.0", "@types/sanitize-html": "^2.11.0",
"@types/serialize-javascript": "^5.0.4", "@types/serialize-javascript": "^5.0.4",
"@types/toastify-js": "^1.12.3", "@types/toastify-js": "^1.12.3",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-inferno": "^7.33.3", "eslint-plugin-inferno": "^7.33.3",
"eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-jsx-a11y": "^6.8.0",
@ -117,15 +117,15 @@
"prettier-plugin-import-sort": "^0.0.7", "prettier-plugin-import-sort": "^0.0.7",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-packagejson": "^2.4.12", "prettier-plugin-packagejson": "^2.4.12",
"qs": "^6.11.2", "qs": "^6.12.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"sortpack": "^2.4.0", "sortpack": "^2.4.0",
"style-loader": "^3.3.4", "style-loader": "^3.3.4",
"terser": "^5.28.1", "terser": "^5.29.2",
"typescript": "^5.3.3", "typescript": "^5.4.3",
"typescript-language-server": "^4.3.3", "typescript-language-server": "^4.3.3",
"webpack-bundle-analyzer": "^4.10.1", "webpack-bundle-analyzer": "^4.10.1",
"webpack-dev-server": "5.0.2" "webpack-dev-server": "5.0.4"
}, },
"lint-staged": { "lint-staged": {
"*.{css, scss}": [ "*.{css, scss}": [

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,6 @@ import {
CommentId, CommentId,
CommentReplyView, CommentReplyView,
CommentResponse, CommentResponse,
CommentView,
CommunityModeratorView, CommunityModeratorView,
CreateComment, CreateComment,
CreateCommentLike, CreateCommentLike,
@ -37,6 +36,7 @@ import deepEqual from "lodash.isequal";
import { commentTreeMaxDepth } from "../../config"; import { commentTreeMaxDepth } from "../../config";
import { import {
CommentNodeI, CommentNodeI,
CommentNodeView,
CommentViewType, CommentViewType,
VoteContentType, VoteContentType,
} from "../../interfaces"; } from "../../interfaces";
@ -152,7 +152,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.handleTransferCommunity = this.handleTransferCommunity.bind(this); this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
} }
get commentView(): CommentView { get commentView(): CommentNodeView {
return this.props.node.comment_view; return this.props.node.comment_view;
} }
@ -188,6 +188,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
post, post,
counts, counts,
my_vote, my_vote,
banned_from_community,
} = this.commentView; } = this.commentView;
const moreRepliesBorderColor = this.props.node.depth const moreRepliesBorderColor = this.props.node.depth
@ -344,54 +345,55 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
</button> </button>
)} )}
{UserService.Instance.myUserInfo && !this.props.viewOnly && ( {UserService.Instance.myUserInfo &&
<> !(this.props.viewOnly || banned_from_community) && (
<VoteButtonsCompact <>
voteContentType={VoteContentType.Comment} <VoteButtonsCompact
id={id} voteContentType={VoteContentType.Comment}
onVote={this.props.onCommentVote} id={id}
enableDownvotes={this.props.enableDownvotes} onVote={this.props.onCommentVote}
counts={counts} enableDownvotes={this.props.enableDownvotes}
my_vote={my_vote} counts={counts}
/> my_vote={my_vote}
<button
type="button"
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, handleToggleViewSource)}
data-tippy-content={I18NextService.i18n.t(
"view_source",
)}
aria-label={I18NextService.i18n.t("view_source")}
>
<Icon
icon="file-text"
classes={`icon-inline ${
this.state.viewSource && "text-success"
}`}
/> />
</button> <button
<CommentActionDropdown type="button"
commentView={this.commentView} className="btn btn-link btn-animate text-muted"
admins={this.props.admins} onClick={linkEvent(this, handleToggleViewSource)}
moderators={this.props.moderators} data-tippy-content={I18NextService.i18n.t(
onReply={this.handleReplyClick} "view_source",
onReport={this.handleReportComment} )}
onBlock={this.handleBlockPerson} aria-label={I18NextService.i18n.t("view_source")}
onSave={this.handleSaveComment} >
onEdit={this.handleEditClick} <Icon
onDelete={this.handleDeleteComment} icon="file-text"
onDistinguish={this.handleDistinguishComment} classes={`icon-inline ${
onRemove={this.handleRemoveComment} this.state.viewSource && "text-success"
onBanFromCommunity={this.handleBanFromCommunity} }`}
onAppointCommunityMod={this.handleAppointCommunityMod} />
onTransferCommunity={this.handleTransferCommunity} </button>
onPurgeUser={this.handlePurgePerson} <CommentActionDropdown
onPurgeContent={this.handlePurgeComment} commentView={this.commentView}
onBanFromSite={this.handleBanFromSite} admins={this.props.admins}
onAppointAdmin={this.handleAppointAdmin} moderators={this.props.moderators}
/> onReply={this.handleReplyClick}
</> onReport={this.handleReportComment}
)} onBlock={this.handleBlockPerson}
onSave={this.handleSaveComment}
onEdit={this.handleEditClick}
onDelete={this.handleDeleteComment}
onDistinguish={this.handleDistinguishComment}
onRemove={this.handleRemoveComment}
onBanFromCommunity={this.handleBanFromCommunity}
onAppointCommunityMod={this.handleAppointCommunityMod}
onTransferCommunity={this.handleTransferCommunity}
onPurgeUser={this.handlePurgePerson}
onPurgeContent={this.handlePurgeComment}
onBanFromSite={this.handleBanFromSite}
onAppointAdmin={this.handleAppointAdmin}
/>
</>
)}
</div> </div>
</> </>
)} )}
@ -589,16 +591,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.setState({ showReply: false, showEdit: false }); this.setState({ showReply: false, showEdit: false });
} }
isPersonMentionType( isPersonMentionType(item: CommentNodeView): item is PersonMentionView {
item: CommentView | PersonMentionView | CommentReplyView, return item.person_mention?.id !== undefined;
): item is PersonMentionView {
return (item as PersonMentionView).person_mention?.id !== undefined;
} }
isCommentReplyType( isCommentReplyType(item: CommentNodeView): item is CommentReplyView {
item: CommentView | PersonMentionView | CommentReplyView, return item.comment_reply?.id !== undefined;
): item is CommentReplyView {
return (item as CommentReplyView).comment_reply?.id !== undefined;
} }
get isCommentNew(): boolean { get isCommentNew(): boolean {

View file

@ -63,6 +63,7 @@ export class CommentReport extends Component<
saved: false, saved: false,
creator_blocked: false, creator_blocked: false,
my_vote: r.my_vote, my_vote: r.my_vote,
banned_from_community: false,
}; };
const node: CommentNodeI = { const node: CommentNodeI = {

View file

@ -3,12 +3,7 @@ import { I18NextService, UserService } from "../../../services";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { CrossPostParams } from "@utils/types"; import { CrossPostParams } from "@utils/types";
import CrossPostButton from "./cross-post-button"; import CrossPostButton from "./cross-post-button";
import { import { CommunityModeratorView, PersonView, PostView } from "lemmy-js-client";
CommentView,
CommunityModeratorView,
PersonView,
PostView,
} from "lemmy-js-client";
import { import {
amAdmin, amAdmin,
amCommunityCreator, amCommunityCreator,
@ -23,7 +18,7 @@ import { Link } from "inferno-router";
import ConfirmationModal from "../confirmation-modal"; import ConfirmationModal from "../confirmation-modal";
import ViewVotesModal from "../view-votes-modal"; import ViewVotesModal from "../view-votes-modal";
import ModActionFormModal, { BanUpdateForm } from "../mod-action-form-modal"; import ModActionFormModal, { BanUpdateForm } from "../mod-action-form-modal";
import { BanType, PurgeType } from "../../../interfaces"; import { BanType, CommentNodeView, PurgeType } from "../../../interfaces";
import { getApubName, hostname } from "@utils/helpers"; import { getApubName, hostname } from "@utils/helpers";
interface ContentActionDropdownPropsBase { interface ContentActionDropdownPropsBase {
@ -46,7 +41,7 @@ interface ContentActionDropdownPropsBase {
export type ContentCommentProps = { export type ContentCommentProps = {
type: "comment"; type: "comment";
commentView: CommentView; commentView: CommentNodeView;
onReply: () => void; onReply: () => void;
onDistinguish: () => Promise<void>; onDistinguish: () => Promise<void>;
} & ContentActionDropdownPropsBase; } & ContentActionDropdownPropsBase;

View file

@ -126,6 +126,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
const { const {
community: { name, actor_id, id, posting_restricted_to_mods, visibility }, community: { name, actor_id, id, posting_restricted_to_mods, visibility },
counts, counts,
banned_from_community,
} = this.props.community_view; } = this.props.community_view;
return ( return (
<aside className="mb-3"> <aside className="mb-3">
@ -134,14 +135,18 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<div className="card-body"> <div className="card-body">
{this.communityTitle()} {this.communityTitle()}
{this.props.editable && this.adminButtons()} {this.props.editable && this.adminButtons()}
<SubscribeButton {!banned_from_community && (
communityView={this.props.community_view} <>
onFollow={linkEvent(this, this.handleFollowCommunity)} <SubscribeButton
onUnFollow={linkEvent(this, this.handleUnfollowCommunity)} communityView={this.props.community_view}
loading={this.state.followCommunityLoading} onFollow={linkEvent(this, this.handleFollowCommunity)}
/> onUnFollow={linkEvent(this, this.handleUnfollowCommunity)}
{this.canPost && this.createPost()} loading={this.state.followCommunityLoading}
{myUserInfo && this.blockCommunity()} />
{this.canPost && this.createPost()}
{myUserInfo && this.blockCommunity()}
</>
)}
{!myUserInfo && ( {!myUserInfo && (
<div className="alert alert-info" role="alert"> <div className="alert alert-info" role="alert">
<T <T
@ -174,6 +179,21 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</T> </T>
</div> </div>
)} )}
{banned_from_community && (
<div
className="alert alert-danger text-sm-start text-xs-center"
role="alert"
>
<Icon
icon="ban"
inline
classes="me-sm-2 mx-auto d-sm-inline d-block"
/>
<T i18nKey="banned_from_community_blurb" className="d-inline">
#<strong className="fw-bold">#</strong>#
</T>
</div>
)}
{this.description()} {this.description()}
<div> <div>
<div className="fw-semibold mb-1"> <div className="fw-semibold mb-1">

View file

@ -547,14 +547,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
commentsLine(mobile = false) { commentsLine(mobile = false) {
const { const { admins, moderators, showBody, onPostVote, enableDownvotes } =
admins, this.props;
moderators,
viewOnly,
showBody,
onPostVote,
enableDownvotes,
} = this.props;
const { const {
post: { ap_id, id, body }, post: { ap_id, id, body },
counts, counts,
@ -580,7 +574,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
> >
<Icon icon="fedilink" inline /> <Icon icon="fedilink" inline />
</a> </a>
{mobile && !viewOnly && ( {mobile && this.isInteractable && (
<VoteButtonsCompact <VoteButtonsCompact
voteContentType={VoteContentType.Post} voteContentType={VoteContentType.Post}
id={id} id={id}
@ -593,7 +587,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{showBody && body && this.viewSourceButton} {showBody && body && this.viewSourceButton}
{UserService.Instance.myUserInfo && !viewOnly && ( {UserService.Instance.myUserInfo && this.isInteractable && (
<PostActionDropdown <PostActionDropdown
postView={this.postView} postView={this.postView}
admins={admins} admins={admins}
@ -734,7 +728,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{/* The larger view*/} {/* The larger view*/}
<div className="d-none d-sm-block"> <div className="d-none d-sm-block">
<article className="row post-container"> <article className="row post-container">
{!this.props.viewOnly && ( {this.isInteractable && (
<div className="col flex-grow-0"> <div className="col flex-grow-0">
<VoteButtons <VoteButtons
voteContentType={VoteContentType.Post} voteContentType={VoteContentType.Post}
@ -1047,4 +1041,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
get canAdmin(): boolean { get canAdmin(): boolean {
return canAdmin(this.postView.creator.id, this.props.admins); return canAdmin(this.postView.creator.id, this.props.admins);
} }
get isInteractable() {
const {
viewOnly,
post_view: { banned_from_community },
} = this.props;
return !(viewOnly || banned_from_community);
}
} }

View file

@ -59,6 +59,8 @@ export class PostReport extends Component<PostReportProps, PostReportState> {
unread_comments: 0, unread_comments: 0,
creator_is_moderator: false, creator_is_moderator: false,
creator_is_admin: false, creator_is_admin: false,
banned_from_community: false,
hidden: false,
}; };
return ( return (

View file

@ -399,7 +399,9 @@ export class Post extends Component<any, PostState> {
<div ref={this.state.commentSectionRef} className="mb-2" /> <div ref={this.state.commentSectionRef} className="mb-2" />
{/* Only show the top level comment form if its not a context view */} {/* Only show the top level comment form if its not a context view */}
{!this.state.commentId && ( {!(
this.state.commentId || res.post_view.banned_from_community
) && (
<CommentForm <CommentForm
node={res.post_view.post.id} node={res.post_view.post.id}
disabled={res.post_view.post.locked} disabled={res.post_view.post.locked}

View file

@ -1,5 +1,10 @@
import { ErrorPageData } from "@utils/types"; import { ErrorPageData } from "@utils/types";
import { CommentView, GetSiteResponse } from "lemmy-js-client"; import {
CommentReply,
CommentView,
GetSiteResponse,
PersonMention,
} from "lemmy-js-client";
import type { ParsedQs } from "qs"; import type { ParsedQs } from "qs";
import { RequestState } from "./services/HttpService"; import { RequestState } from "./services/HttpService";
@ -77,8 +82,14 @@ export enum VoteContentType {
Comment, Comment,
} }
export type CommentNodeView = Omit<CommentView, "banned_from_community"> &
Partial<Pick<CommentView, "banned_from_community">> & {
person_mention?: PersonMention;
comment_reply?: CommentReply;
};
export interface CommentNodeI { export interface CommentNodeI {
comment_view: CommentView; comment_view: CommentNodeView;
children: Array<CommentNodeI>; children: Array<CommentNodeI>;
depth: number; depth: number;
} }

View file

@ -1,8 +1,7 @@
import { CommentView } from "lemmy-js-client"; import { CommentNodeI, CommentNodeView } from "../../interfaces";
import { CommentNodeI } from "../../interfaces";
export default function commentsToFlatNodes( export default function commentsToFlatNodes(
comments: CommentView[], comments: CommentNodeView[],
): CommentNodeI[] { ): CommentNodeI[] {
const nodes: CommentNodeI[] = []; const nodes: CommentNodeI[] = [];
for (const comment of comments) { for (const comment of comments) {