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