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