]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/sidebar.tsx
Merge branch 'main' of https://github.com/LemmyNet/lemmy-ui
[lemmy-ui.git] / src / shared / components / community / sidebar.tsx
1 import { Component, InfernoNode, linkEvent } from "inferno";
2 import { T } from "inferno-i18next-dess";
3 import { Link } from "inferno-router";
4 import {
5   AddModToCommunity,
6   BlockCommunity,
7   CommunityModeratorView,
8   CommunityView,
9   DeleteCommunity,
10   EditCommunity,
11   FollowCommunity,
12   Language,
13   PersonView,
14   PurgeCommunity,
15   RemoveCommunity,
16 } from "lemmy-js-client";
17 import { i18n } from "../../i18next";
18 import { UserService } from "../../services";
19 import {
20   amAdmin,
21   amMod,
22   amTopMod,
23   getUnixTime,
24   hostname,
25   mdToHtml,
26   myAuthRequired,
27   numToSI,
28 } from "../../utils";
29 import { BannerIconHeader } from "../common/banner-icon-header";
30 import { Icon, PurgeWarning, Spinner } from "../common/icon";
31 import { CommunityForm } from "../community/community-form";
32 import { CommunityLink } from "../community/community-link";
33 import { PersonListing } from "../person/person-listing";
34
35 interface SidebarProps {
36   community_view: CommunityView;
37   moderators: CommunityModeratorView[];
38   admins: PersonView[];
39   allLanguages: Language[];
40   siteLanguages: number[];
41   communityLanguages?: number[];
42   online: number;
43   enableNsfw?: boolean;
44   showIcon?: boolean;
45   editable?: boolean;
46   onDeleteCommunity(form: DeleteCommunity): void;
47   onRemoveCommunity(form: RemoveCommunity): void;
48   onLeaveModTeam(form: AddModToCommunity): void;
49   onFollowCommunity(form: FollowCommunity): void;
50   onBlockCommunity(form: BlockCommunity): void;
51   onPurgeCommunity(form: PurgeCommunity): void;
52   onEditCommunity(form: EditCommunity): void;
53 }
54
55 interface SidebarState {
56   removeReason?: string;
57   removeExpires?: string;
58   showEdit: boolean;
59   showRemoveDialog: boolean;
60   showPurgeDialog: boolean;
61   purgeReason?: string;
62   showConfirmLeaveModTeam: boolean;
63   deleteCommunityLoading: boolean;
64   removeCommunityLoading: boolean;
65   leaveModTeamLoading: boolean;
66   followCommunityLoading: boolean;
67   blockCommunityLoading: boolean;
68   purgeCommunityLoading: boolean;
69 }
70
71 export class Sidebar extends Component<SidebarProps, SidebarState> {
72   state: SidebarState = {
73     showEdit: false,
74     showRemoveDialog: false,
75     showPurgeDialog: false,
76     showConfirmLeaveModTeam: false,
77     deleteCommunityLoading: false,
78     removeCommunityLoading: false,
79     leaveModTeamLoading: false,
80     followCommunityLoading: false,
81     blockCommunityLoading: false,
82     purgeCommunityLoading: false,
83   };
84
85   constructor(props: any, context: any) {
86     super(props, context);
87     this.handleEditCancel = this.handleEditCancel.bind(this);
88   }
89
90   componentWillReceiveProps(
91     nextProps: Readonly<{ children?: InfernoNode } & SidebarProps>
92   ): void {
93     if (this.props.moderators != nextProps.moderators) {
94       this.setState({
95         showConfirmLeaveModTeam: false,
96       });
97     }
98
99     if (this.props.community_view != nextProps.community_view) {
100       this.setState({
101         showEdit: false,
102         showPurgeDialog: false,
103         showRemoveDialog: false,
104         deleteCommunityLoading: false,
105         removeCommunityLoading: false,
106         leaveModTeamLoading: false,
107         followCommunityLoading: false,
108         blockCommunityLoading: false,
109         purgeCommunityLoading: false,
110       });
111     }
112   }
113
114   render() {
115     return (
116       <div>
117         {!this.state.showEdit ? (
118           this.sidebar()
119         ) : (
120           <CommunityForm
121             community_view={this.props.community_view}
122             allLanguages={this.props.allLanguages}
123             siteLanguages={this.props.siteLanguages}
124             communityLanguages={this.props.communityLanguages}
125             onUpsertCommunity={this.props.onEditCommunity}
126             onCancel={this.handleEditCancel}
127             enableNsfw={this.props.enableNsfw}
128           />
129         )}
130       </div>
131     );
132   }
133
134   sidebar() {
135     const myUSerInfo = UserService.Instance.myUserInfo;
136     const { name, actor_id } = this.props.community_view.community;
137     return (
138       <div>
139         <div className="card border-secondary mb-3">
140           <div className="card-body">
141             {this.communityTitle()}
142             {this.props.editable && this.adminButtons()}
143             {myUSerInfo && this.subscribe()}
144             {this.canPost && this.createPost()}
145             {myUSerInfo && this.blockCommunity()}
146             {!myUSerInfo && (
147               <div className="alert alert-info" role="alert">
148                 <T
149                   i18nKey="community_not_logged_in_alert"
150                   interpolation={{
151                     community: name,
152                     instance: hostname(actor_id),
153                   }}
154                 >
155                   #<code className="user-select-all">#</code>#
156                 </T>
157               </div>
158             )}
159           </div>
160         </div>
161         <div className="card border-secondary mb-3">
162           <div className="card-body">
163             {this.description()}
164             {this.badges()}
165             {this.mods()}
166           </div>
167         </div>
168       </div>
169     );
170   }
171
172   communityTitle() {
173     const community = this.props.community_view.community;
174     const subscribed = this.props.community_view.subscribed;
175     return (
176       <div>
177         <h5 className="mb-0">
178           {this.props.showIcon && !community.removed && (
179             <BannerIconHeader icon={community.icon} banner={community.banner} />
180           )}
181           <span className="mr-2">
182             <CommunityLink community={community} hideAvatar />
183           </span>
184           {subscribed === "Subscribed" && (
185             <button
186               className="btn btn-secondary btn-sm mr-2"
187               onClick={linkEvent(this, this.handleUnfollowCommunity)}
188             >
189               {this.state.followCommunityLoading ? (
190                 <Spinner />
191               ) : (
192                 <>
193                   <Icon icon="check" classes="icon-inline text-success mr-1" />
194                   {i18n.t("joined")}
195                 </>
196               )}
197             </button>
198           )}
199           {subscribed === "Pending" && (
200             <button
201               className="btn btn-warning mr-2"
202               onClick={linkEvent(this, this.handleUnfollowCommunity)}
203             >
204               {this.state.followCommunityLoading ? (
205                 <Spinner />
206               ) : (
207                 i18n.t("subscribe_pending")
208               )}
209             </button>
210           )}
211           {community.removed && (
212             <small className="mr-2 text-muted font-italic">
213               {i18n.t("removed")}
214             </small>
215           )}
216           {community.deleted && (
217             <small className="mr-2 text-muted font-italic">
218               {i18n.t("deleted")}
219             </small>
220           )}
221           {community.nsfw && (
222             <small className="mr-2 text-muted font-italic">
223               {i18n.t("nsfw")}
224             </small>
225           )}
226         </h5>
227         <CommunityLink
228           community={community}
229           realLink
230           useApubName
231           muted
232           hideAvatar
233         />
234       </div>
235     );
236   }
237
238   badges() {
239     const community_view = this.props.community_view;
240     const counts = community_view.counts;
241     return (
242       <ul className="my-1 list-inline">
243         <li className="list-inline-item badge badge-secondary">
244           {i18n.t("number_online", {
245             count: this.props.online,
246             formattedCount: numToSI(this.props.online),
247           })}
248         </li>
249         <li
250           className="list-inline-item badge badge-secondary pointer"
251           data-tippy-content={i18n.t("active_users_in_the_last_day", {
252             count: Number(counts.users_active_day),
253             formattedCount: numToSI(counts.users_active_day),
254           })}
255         >
256           {i18n.t("number_of_users", {
257             count: Number(counts.users_active_day),
258             formattedCount: numToSI(counts.users_active_day),
259           })}{" "}
260           / {i18n.t("day")}
261         </li>
262         <li
263           className="list-inline-item badge badge-secondary pointer"
264           data-tippy-content={i18n.t("active_users_in_the_last_week", {
265             count: Number(counts.users_active_week),
266             formattedCount: numToSI(counts.users_active_week),
267           })}
268         >
269           {i18n.t("number_of_users", {
270             count: Number(counts.users_active_week),
271             formattedCount: numToSI(counts.users_active_week),
272           })}{" "}
273           / {i18n.t("week")}
274         </li>
275         <li
276           className="list-inline-item badge badge-secondary pointer"
277           data-tippy-content={i18n.t("active_users_in_the_last_month", {
278             count: Number(counts.users_active_month),
279             formattedCount: numToSI(counts.users_active_month),
280           })}
281         >
282           {i18n.t("number_of_users", {
283             count: Number(counts.users_active_month),
284             formattedCount: numToSI(counts.users_active_month),
285           })}{" "}
286           / {i18n.t("month")}
287         </li>
288         <li
289           className="list-inline-item badge badge-secondary pointer"
290           data-tippy-content={i18n.t("active_users_in_the_last_six_months", {
291             count: Number(counts.users_active_half_year),
292             formattedCount: numToSI(counts.users_active_half_year),
293           })}
294         >
295           {i18n.t("number_of_users", {
296             count: Number(counts.users_active_half_year),
297             formattedCount: numToSI(counts.users_active_half_year),
298           })}{" "}
299           / {i18n.t("number_of_months", { count: 6, formattedCount: 6 })}
300         </li>
301         <li className="list-inline-item badge badge-secondary">
302           {i18n.t("number_of_subscribers", {
303             count: Number(counts.subscribers),
304             formattedCount: numToSI(counts.subscribers),
305           })}
306         </li>
307         <li className="list-inline-item badge badge-secondary">
308           {i18n.t("number_of_posts", {
309             count: Number(counts.posts),
310             formattedCount: numToSI(counts.posts),
311           })}
312         </li>
313         <li className="list-inline-item badge badge-secondary">
314           {i18n.t("number_of_comments", {
315             count: Number(counts.comments),
316             formattedCount: numToSI(counts.comments),
317           })}
318         </li>
319         <li className="list-inline-item">
320           <Link
321             className="badge badge-primary"
322             to={`/modlog/${this.props.community_view.community.id}`}
323           >
324             {i18n.t("modlog")}
325           </Link>
326         </li>
327       </ul>
328     );
329   }
330
331   mods() {
332     return (
333       <ul className="list-inline small">
334         <li className="list-inline-item">{i18n.t("mods")}: </li>
335         {this.props.moderators.map(mod => (
336           <li key={mod.moderator.id} className="list-inline-item">
337             <PersonListing person={mod.moderator} />
338           </li>
339         ))}
340       </ul>
341     );
342   }
343
344   createPost() {
345     const cv = this.props.community_view;
346     return (
347       <Link
348         className={`btn btn-secondary btn-block mb-2 ${
349           cv.community.deleted || cv.community.removed ? "no-click" : ""
350         }`}
351         to={`/create_post?communityId=${cv.community.id}`}
352       >
353         {i18n.t("create_a_post")}
354       </Link>
355     );
356   }
357
358   subscribe() {
359     const community_view = this.props.community_view;
360     return (
361       <div className="mb-2">
362         {community_view.subscribed == "NotSubscribed" && (
363           <button
364             className="btn btn-secondary btn-block"
365             onClick={linkEvent(this, this.handleFollowCommunity)}
366           >
367             {this.state.followCommunityLoading ? (
368               <Spinner />
369             ) : (
370               i18n.t("subscribe")
371             )}
372           </button>
373         )}
374       </div>
375     );
376   }
377
378   blockCommunity() {
379     const community_view = this.props.community_view;
380     const blocked = this.props.community_view.blocked;
381
382     return (
383       <div className="mb-2">
384         {community_view.subscribed == "NotSubscribed" &&
385           (blocked ? (
386             <button
387               className="btn btn-danger btn-block"
388               onClick={linkEvent(this, this.handleBlockCommunity)}
389             >
390               {this.state.blockCommunityLoading ? (
391                 <Spinner />
392               ) : (
393                 i18n.t("unblock_community")
394               )}
395             </button>
396           ) : (
397             <button
398               className="btn btn-danger btn-block"
399               onClick={linkEvent(this, this.handleBlockCommunity)}
400             >
401               {this.state.blockCommunityLoading ? (
402                 <Spinner />
403               ) : (
404                 i18n.t("block_community")
405               )}
406             </button>
407           ))}
408       </div>
409     );
410   }
411
412   description() {
413     const desc = this.props.community_view.community.description;
414     return (
415       desc && (
416         <div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} />
417       )
418     );
419   }
420
421   adminButtons() {
422     const community_view = this.props.community_view;
423     return (
424       <>
425         <ul className="list-inline mb-1 text-muted font-weight-bold">
426           {amMod(this.props.moderators) && (
427             <>
428               <li className="list-inline-item-action">
429                 <button
430                   className="btn btn-link text-muted d-inline-block"
431                   onClick={linkEvent(this, this.handleEditClick)}
432                   data-tippy-content={i18n.t("edit")}
433                   aria-label={i18n.t("edit")}
434                 >
435                   <Icon icon="edit" classes="icon-inline" />
436                 </button>
437               </li>
438               {!amTopMod(this.props.moderators) &&
439                 (!this.state.showConfirmLeaveModTeam ? (
440                   <li className="list-inline-item-action">
441                     <button
442                       className="btn btn-link text-muted d-inline-block"
443                       onClick={linkEvent(
444                         this,
445                         this.handleShowConfirmLeaveModTeamClick
446                       )}
447                     >
448                       {i18n.t("leave_mod_team")}
449                     </button>
450                   </li>
451                 ) : (
452                   <>
453                     <li className="list-inline-item-action">
454                       {i18n.t("are_you_sure")}
455                     </li>
456                     <li className="list-inline-item-action">
457                       <button
458                         className="btn btn-link text-muted d-inline-block"
459                         onClick={linkEvent(this, this.handleLeaveModTeam)}
460                       >
461                         {i18n.t("yes")}
462                       </button>
463                     </li>
464                     <li className="list-inline-item-action">
465                       <button
466                         className="btn btn-link text-muted d-inline-block"
467                         onClick={linkEvent(
468                           this,
469                           this.handleCancelLeaveModTeamClick
470                         )}
471                       >
472                         {i18n.t("no")}
473                       </button>
474                     </li>
475                   </>
476                 ))}
477               {amTopMod(this.props.moderators) && (
478                 <li className="list-inline-item-action">
479                   <button
480                     className="btn btn-link text-muted d-inline-block"
481                     onClick={linkEvent(this, this.handleDeleteCommunity)}
482                     data-tippy-content={
483                       !community_view.community.deleted
484                         ? i18n.t("delete")
485                         : i18n.t("restore")
486                     }
487                     aria-label={
488                       !community_view.community.deleted
489                         ? i18n.t("delete")
490                         : i18n.t("restore")
491                     }
492                   >
493                     {this.state.deleteCommunityLoading ? (
494                       <Spinner />
495                     ) : (
496                       <Icon
497                         icon="trash"
498                         classes={`icon-inline ${
499                           community_view.community.deleted && "text-danger"
500                         }`}
501                       />
502                     )}{" "}
503                   </button>
504                 </li>
505               )}
506             </>
507           )}
508           {amAdmin() && (
509             <li className="list-inline-item">
510               {!this.props.community_view.community.removed ? (
511                 <button
512                   className="btn btn-link text-muted d-inline-block"
513                   onClick={linkEvent(this, this.handleModRemoveShow)}
514                 >
515                   {i18n.t("remove")}
516                 </button>
517               ) : (
518                 <button
519                   className="btn btn-link text-muted d-inline-block"
520                   onClick={linkEvent(this, this.handleRemoveCommunity)}
521                 >
522                   {this.state.removeCommunityLoading ? (
523                     <Spinner />
524                   ) : (
525                     i18n.t("restore")
526                   )}
527                 </button>
528               )}
529               <button
530                 className="btn btn-link text-muted d-inline-block"
531                 onClick={linkEvent(this, this.handlePurgeCommunityShow)}
532                 aria-label={i18n.t("purge_community")}
533               >
534                 {i18n.t("purge_community")}
535               </button>
536             </li>
537           )}
538         </ul>
539         {this.state.showRemoveDialog && (
540           <form onSubmit={linkEvent(this, this.handleRemoveCommunity)}>
541             <div className="form-group">
542               <label className="col-form-label" htmlFor="remove-reason">
543                 {i18n.t("reason")}
544               </label>
545               <input
546                 type="text"
547                 id="remove-reason"
548                 className="form-control mr-2"
549                 placeholder={i18n.t("optional")}
550                 value={this.state.removeReason}
551                 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
552               />
553             </div>
554             {/* TODO hold off on expires for now */}
555             {/* <div class="form-group row"> */}
556             {/*   <label class="col-form-label">Expires</label> */}
557             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
558             {/* </div> */}
559             <div className="form-group">
560               <button type="submit" className="btn btn-secondary">
561                 {this.state.removeCommunityLoading ? (
562                   <Spinner />
563                 ) : (
564                   i18n.t("remove_community")
565                 )}
566               </button>
567             </div>
568           </form>
569         )}
570         {this.state.showPurgeDialog && (
571           <form onSubmit={linkEvent(this, this.handlePurgeCommunity)}>
572             <div className="form-group">
573               <PurgeWarning />
574             </div>
575             <div className="form-group">
576               <label className="sr-only" htmlFor="purge-reason">
577                 {i18n.t("reason")}
578               </label>
579               <input
580                 type="text"
581                 id="purge-reason"
582                 className="form-control mr-2"
583                 placeholder={i18n.t("reason")}
584                 value={this.state.purgeReason}
585                 onInput={linkEvent(this, this.handlePurgeReasonChange)}
586               />
587             </div>
588             <div className="form-group">
589               {this.state.purgeCommunityLoading ? (
590                 <Spinner />
591               ) : (
592                 <button
593                   type="submit"
594                   className="btn btn-secondary"
595                   aria-label={i18n.t("purge_community")}
596                 >
597                   {i18n.t("purge_community")}
598                 </button>
599               )}
600             </div>
601           </form>
602         )}
603       </>
604     );
605   }
606
607   handleEditClick(i: Sidebar) {
608     i.setState({ showEdit: true });
609   }
610
611   handleEditCancel() {
612     this.setState({ showEdit: false });
613   }
614
615   handleShowConfirmLeaveModTeamClick(i: Sidebar) {
616     i.setState({ showConfirmLeaveModTeam: true });
617   }
618
619   handleCancelLeaveModTeamClick(i: Sidebar) {
620     i.setState({ showConfirmLeaveModTeam: false });
621   }
622
623   get canPost(): boolean {
624     return (
625       !this.props.community_view.community.posting_restricted_to_mods ||
626       amMod(this.props.moderators) ||
627       amAdmin()
628     );
629   }
630
631   handleModRemoveShow(i: Sidebar) {
632     i.setState({ showRemoveDialog: true });
633   }
634
635   handleModRemoveReasonChange(i: Sidebar, event: any) {
636     i.setState({ removeReason: event.target.value });
637   }
638
639   handleModRemoveExpiresChange(i: Sidebar, event: any) {
640     i.setState({ removeExpires: event.target.value });
641   }
642
643   handlePurgeCommunityShow(i: Sidebar) {
644     i.setState({ showPurgeDialog: true, showRemoveDialog: false });
645   }
646
647   handlePurgeReasonChange(i: Sidebar, event: any) {
648     i.setState({ purgeReason: event.target.value });
649   }
650
651   // TODO Do we need two of these?
652   handleUnfollowCommunity(i: Sidebar) {
653     i.setState({ followCommunityLoading: true });
654     i.props.onFollowCommunity({
655       community_id: i.props.community_view.community.id,
656       follow: false,
657       auth: myAuthRequired(),
658     });
659   }
660
661   handleFollowCommunity(i: Sidebar) {
662     i.setState({ followCommunityLoading: true });
663     i.props.onFollowCommunity({
664       community_id: i.props.community_view.community.id,
665       follow: true,
666       auth: myAuthRequired(),
667     });
668   }
669
670   handleBlockCommunity(i: Sidebar) {
671     i.setState({ blockCommunityLoading: true });
672     i.props.onBlockCommunity({
673       community_id: 0,
674       block: !i.props.community_view.blocked,
675       auth: myAuthRequired(),
676     });
677   }
678
679   handleLeaveModTeam(i: Sidebar) {
680     const myId = UserService.Instance.myUserInfo?.local_user_view.person.id;
681     if (myId) {
682       i.setState({ leaveModTeamLoading: true });
683       i.props.onLeaveModTeam({
684         community_id: i.props.community_view.community.id,
685         person_id: 92,
686         added: false,
687         auth: myAuthRequired(),
688       });
689     }
690   }
691
692   handleDeleteCommunity(i: Sidebar) {
693     i.setState({ deleteCommunityLoading: true });
694     i.props.onDeleteCommunity({
695       community_id: i.props.community_view.community.id,
696       deleted: !i.props.community_view.community.deleted,
697       auth: myAuthRequired(),
698     });
699   }
700
701   handleRemoveCommunity(i: Sidebar, event: any) {
702     event.preventDefault();
703     i.setState({ removeCommunityLoading: true });
704     i.props.onRemoveCommunity({
705       community_id: i.props.community_view.community.id,
706       removed: !i.props.community_view.community.removed,
707       reason: i.state.removeReason,
708       expires: getUnixTime(i.state.removeExpires), // TODO fix this
709       auth: myAuthRequired(),
710     });
711   }
712
713   handlePurgeCommunity(i: Sidebar, event: any) {
714     event.preventDefault();
715     i.setState({ purgeCommunityLoading: true });
716     i.props.onPurgeCommunity({
717       community_id: i.props.community_view.community.id,
718       reason: i.state.purgeReason,
719       auth: myAuthRequired(),
720     });
721   }
722 }