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