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