1 import { None, Option, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
7 CommunityModeratorView,
17 } from "lemmy-js-client";
18 import { i18n } from "../../i18next";
19 import { UserService, WebSocketService } from "../../services";
30 import { BannerIconHeader } from "../common/banner-icon-header";
31 import { Icon, PurgeWarning, Spinner } from "../common/icon";
32 import { CommunityForm } from "../community/community-form";
33 import { CommunityLink } from "../community/community-link";
34 import { PersonListing } from "../person/person-listing";
36 interface SidebarProps {
37 community_view: CommunityView;
38 moderators: CommunityModeratorView[];
39 admins: PersonViewSafe[];
40 allLanguages: Language[];
41 siteLanguages: number[];
42 communityLanguages: Option<number[]>;
49 interface SidebarState {
50 removeReason: Option<string>;
51 removeExpires: Option<string>;
53 showRemoveDialog: boolean;
54 showPurgeDialog: boolean;
55 purgeReason: Option<string>;
56 purgeLoading: boolean;
57 showConfirmLeaveModTeam: boolean;
60 export class Sidebar extends Component<SidebarProps, SidebarState> {
61 private emptyState: SidebarState = {
63 showRemoveDialog: false,
66 showPurgeDialog: false,
69 showConfirmLeaveModTeam: false,
72 constructor(props: any, context: any) {
73 super(props, context);
74 this.state = this.emptyState;
75 this.handleEditCommunity = this.handleEditCommunity.bind(this);
76 this.handleEditCancel = this.handleEditCancel.bind(this);
82 {!this.state.showEdit ? (
86 community_view={Some(this.props.community_view)}
87 allLanguages={this.props.allLanguages}
88 siteLanguages={this.props.siteLanguages}
89 communityLanguages={this.props.communityLanguages}
90 onEdit={this.handleEditCommunity}
91 onCancel={this.handleEditCancel}
92 enableNsfw={this.props.enableNsfw}
102 <div className="card border-secondary mb-3">
103 <div className="card-body">
104 {this.communityTitle()}
105 {this.props.editable && this.adminButtons()}
107 {this.canPost && this.createPost()}
108 {this.blockCommunity()}
111 <div className="card border-secondary mb-3">
112 <div className="card-body">
123 let community = this.props.community_view.community;
124 let subscribed = this.props.community_view.subscribed;
127 <h5 className="mb-0">
128 {this.props.showIcon && (
129 <BannerIconHeader icon={community.icon} banner={community.banner} />
131 <span className="mr-2">{community.title}</span>
132 {subscribed == SubscribedType.Subscribed && (
134 className="btn btn-secondary btn-sm mr-2"
135 onClick={linkEvent(this, this.handleUnsubscribe)}
137 <Icon icon="check" classes="icon-inline text-success mr-1" />
141 {subscribed == SubscribedType.Pending && (
143 className="btn btn-warning mr-2"
144 onClick={linkEvent(this, this.handleUnsubscribe)}
146 {i18n.t("subscribe_pending")}
149 {community.removed && (
150 <small className="mr-2 text-muted font-italic">
154 {community.deleted && (
155 <small className="mr-2 text-muted font-italic">
160 <small className="mr-2 text-muted font-italic">
166 community={community}
177 let community_view = this.props.community_view;
178 let counts = community_view.counts;
180 <ul className="my-1 list-inline">
181 <li className="list-inline-item badge badge-secondary">
182 {i18n.t("number_online", {
183 count: this.props.online,
184 formattedCount: numToSI(this.props.online),
188 className="list-inline-item badge badge-secondary pointer"
189 data-tippy-content={i18n.t("active_users_in_the_last_day", {
190 count: counts.users_active_day,
191 formattedCount: counts.users_active_day,
194 {i18n.t("number_of_users", {
195 count: counts.users_active_day,
196 formattedCount: numToSI(counts.users_active_day),
201 className="list-inline-item badge badge-secondary pointer"
202 data-tippy-content={i18n.t("active_users_in_the_last_week", {
203 count: counts.users_active_week,
204 formattedCount: counts.users_active_week,
207 {i18n.t("number_of_users", {
208 count: counts.users_active_week,
209 formattedCount: numToSI(counts.users_active_week),
214 className="list-inline-item badge badge-secondary pointer"
215 data-tippy-content={i18n.t("active_users_in_the_last_month", {
216 count: counts.users_active_month,
217 formattedCount: counts.users_active_month,
220 {i18n.t("number_of_users", {
221 count: counts.users_active_month,
222 formattedCount: numToSI(counts.users_active_month),
227 className="list-inline-item badge badge-secondary pointer"
228 data-tippy-content={i18n.t("active_users_in_the_last_six_months", {
229 count: counts.users_active_half_year,
230 formattedCount: counts.users_active_half_year,
233 {i18n.t("number_of_users", {
234 count: counts.users_active_half_year,
235 formattedCount: numToSI(counts.users_active_half_year),
237 / {i18n.t("number_of_months", { count: 6, formattedCount: 6 })}
239 <li className="list-inline-item badge badge-secondary">
240 {i18n.t("number_of_subscribers", {
241 count: counts.subscribers,
242 formattedCount: numToSI(counts.subscribers),
245 <li className="list-inline-item badge badge-secondary">
246 {i18n.t("number_of_posts", {
248 formattedCount: numToSI(counts.posts),
251 <li className="list-inline-item badge badge-secondary">
252 {i18n.t("number_of_comments", {
253 count: counts.comments,
254 formattedCount: numToSI(counts.comments),
257 <li className="list-inline-item">
259 className="badge badge-primary"
260 to={`/modlog/community/${this.props.community_view.community.id}`}
271 <ul className="list-inline small">
272 <li className="list-inline-item">{i18n.t("mods")}: </li>
273 {this.props.moderators.map(mod => (
274 <li key={mod.moderator.id} className="list-inline-item">
275 <PersonListing person={mod.moderator} />
283 let cv = this.props.community_view;
286 className={`btn btn-secondary btn-block mb-2 ${
287 cv.community.deleted || cv.community.removed ? "no-click" : ""
289 to={`/create_post?community_id=${cv.community.id}`}
291 {i18n.t("create_a_post")}
297 let community_view = this.props.community_view;
299 <div className="mb-2">
300 {community_view.subscribed == SubscribedType.NotSubscribed && (
302 className="btn btn-secondary btn-block"
303 onClick={linkEvent(this, this.handleSubscribe)}
305 {i18n.t("subscribe")}
313 let community_view = this.props.community_view;
314 let blocked = this.props.community_view.blocked;
317 <div className="mb-2">
318 {community_view.subscribed == SubscribedType.NotSubscribed &&
321 className="btn btn-danger btn-block"
322 onClick={linkEvent(this, this.handleUnblock)}
324 {i18n.t("unblock_community")}
328 className="btn btn-danger btn-block"
329 onClick={linkEvent(this, this.handleBlock)}
331 {i18n.t("block_community")}
339 let description = this.props.community_view.community.description;
340 return description.match({
342 <div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} />
349 let community_view = this.props.community_view;
352 <ul className="list-inline mb-1 text-muted font-weight-bold">
353 {amMod(Some(this.props.moderators)) && (
355 <li className="list-inline-item-action">
357 className="btn btn-link text-muted d-inline-block"
358 onClick={linkEvent(this, this.handleEditClick)}
359 data-tippy-content={i18n.t("edit")}
360 aria-label={i18n.t("edit")}
362 <Icon icon="edit" classes="icon-inline" />
365 {!amTopMod(Some(this.props.moderators)) &&
366 (!this.state.showConfirmLeaveModTeam ? (
367 <li className="list-inline-item-action">
369 className="btn btn-link text-muted d-inline-block"
372 this.handleShowConfirmLeaveModTeamClick
375 {i18n.t("leave_mod_team")}
380 <li className="list-inline-item-action">
381 {i18n.t("are_you_sure")}
383 <li className="list-inline-item-action">
385 className="btn btn-link text-muted d-inline-block"
386 onClick={linkEvent(this, this.handleLeaveModTeamClick)}
391 <li className="list-inline-item-action">
393 className="btn btn-link text-muted d-inline-block"
396 this.handleCancelLeaveModTeamClick
404 {amTopMod(Some(this.props.moderators)) && (
405 <li className="list-inline-item-action">
407 className="btn btn-link text-muted d-inline-block"
408 onClick={linkEvent(this, this.handleDeleteClick)}
410 !community_view.community.deleted
415 !community_view.community.deleted
422 classes={`icon-inline ${
423 community_view.community.deleted && "text-danger"
432 <li className="list-inline-item">
433 {!this.props.community_view.community.removed ? (
435 className="btn btn-link text-muted d-inline-block"
436 onClick={linkEvent(this, this.handleModRemoveShow)}
442 className="btn btn-link text-muted d-inline-block"
443 onClick={linkEvent(this, this.handleModRemoveSubmit)}
449 className="btn btn-link text-muted d-inline-block"
450 onClick={linkEvent(this, this.handlePurgeCommunityShow)}
451 aria-label={i18n.t("purge_community")}
453 {i18n.t("purge_community")}
458 {this.state.showRemoveDialog && (
459 <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
460 <div className="form-group">
461 <label className="col-form-label" htmlFor="remove-reason">
467 className="form-control mr-2"
468 placeholder={i18n.t("optional")}
469 value={toUndefined(this.state.removeReason)}
470 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
473 {/* TODO hold off on expires for now */}
474 {/* <div class="form-group row"> */}
475 {/* <label class="col-form-label">Expires</label> */}
476 {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
478 <div className="form-group">
479 <button type="submit" className="btn btn-secondary">
480 {i18n.t("remove_community")}
485 {this.state.showPurgeDialog && (
486 <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
487 <div className="form-group">
490 <div className="form-group">
491 <label className="sr-only" htmlFor="purge-reason">
497 className="form-control mr-2"
498 placeholder={i18n.t("reason")}
499 value={toUndefined(this.state.purgeReason)}
500 onInput={linkEvent(this, this.handlePurgeReasonChange)}
503 <div className="form-group">
504 {this.state.purgeLoading ? (
509 className="btn btn-secondary"
510 aria-label={i18n.t("purge_community")}
512 {i18n.t("purge_community")}
522 handleEditClick(i: Sidebar) {
523 i.setState({ showEdit: true });
526 handleEditCommunity() {
527 this.setState({ showEdit: false });
531 this.setState({ showEdit: false });
534 handleDeleteClick(i: Sidebar, event: any) {
535 event.preventDefault();
536 let deleteForm = new DeleteCommunity({
537 community_id: i.props.community_view.community.id,
538 deleted: !i.props.community_view.community.deleted,
539 auth: auth().unwrap(),
541 WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm));
544 handleShowConfirmLeaveModTeamClick(i: Sidebar) {
545 i.setState({ showConfirmLeaveModTeam: true });
548 handleLeaveModTeamClick(i: Sidebar) {
549 UserService.Instance.myUserInfo.match({
551 let form = new AddModToCommunity({
552 person_id: mui.local_user_view.person.id,
553 community_id: i.props.community_view.community.id,
555 auth: auth().unwrap(),
557 WebSocketService.Instance.send(wsClient.addModToCommunity(form));
558 i.setState({ showConfirmLeaveModTeam: false });
564 handleCancelLeaveModTeamClick(i: Sidebar) {
565 i.setState({ showConfirmLeaveModTeam: false });
568 handleUnsubscribe(i: Sidebar, event: any) {
569 event.preventDefault();
570 let community_id = i.props.community_view.community.id;
571 let form = new FollowCommunity({
574 auth: auth().unwrap(),
576 WebSocketService.Instance.send(wsClient.followCommunity(form));
579 UserService.Instance.myUserInfo.match({
581 (mui.follows = mui.follows.filter(i => i.community.id != community_id)),
586 handleSubscribe(i: Sidebar, event: any) {
587 event.preventDefault();
588 let community_id = i.props.community_view.community.id;
589 let form = new FollowCommunity({
592 auth: auth().unwrap(),
594 WebSocketService.Instance.send(wsClient.followCommunity(form));
597 UserService.Instance.myUserInfo.match({
600 community: i.props.community_view.community,
601 follower: mui.local_user_view.person,
607 get canPost(): boolean {
609 !this.props.community_view.community.posting_restricted_to_mods ||
610 amMod(Some(this.props.moderators)) ||
615 handleModRemoveShow(i: Sidebar) {
616 i.setState({ showRemoveDialog: true });
619 handleModRemoveReasonChange(i: Sidebar, event: any) {
620 i.setState({ removeReason: Some(event.target.value) });
623 handleModRemoveExpiresChange(i: Sidebar, event: any) {
624 i.setState({ removeExpires: Some(event.target.value) });
627 handleModRemoveSubmit(i: Sidebar, event: any) {
628 event.preventDefault();
629 let removeForm = new RemoveCommunity({
630 community_id: i.props.community_view.community.id,
631 removed: !i.props.community_view.community.removed,
632 reason: i.state.removeReason,
633 expires: i.state.removeExpires.map(getUnixTime),
634 auth: auth().unwrap(),
636 WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
638 i.setState({ showRemoveDialog: false });
641 handlePurgeCommunityShow(i: Sidebar) {
642 i.setState({ showPurgeDialog: true, showRemoveDialog: false });
645 handlePurgeReasonChange(i: Sidebar, event: any) {
646 i.setState({ purgeReason: Some(event.target.value) });
649 handlePurgeSubmit(i: Sidebar, event: any) {
650 event.preventDefault();
652 let form = new PurgeCommunity({
653 community_id: i.props.community_view.community.id,
654 reason: i.state.purgeReason,
655 auth: auth().unwrap(),
657 WebSocketService.Instance.send(wsClient.purgeCommunity(form));
659 i.setState({ purgeLoading: true });
662 handleBlock(i: Sidebar, event: any) {
663 event.preventDefault();
664 let blockCommunityForm = new BlockCommunity({
665 community_id: i.props.community_view.community.id,
667 auth: auth().unwrap(),
669 WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm));
672 handleUnblock(i: Sidebar, event: any) {
673 event.preventDefault();
674 let blockCommunityForm = new BlockCommunity({
675 community_id: i.props.community_view.community.id,
677 auth: auth().unwrap(),
679 WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm));