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