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