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