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