]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/sidebar.tsx
Upgrade inferno v8.0.0 try2 (#790)
[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 className="card border-secondary mb-3">
95           <div className="card-body">
96             {this.communityTitle()}
97             {this.adminButtons()}
98             {this.subscribe()}
99             {this.canPost && this.createPost()}
100             {this.blockCommunity()}
101           </div>
102         </div>
103         <div className="card border-secondary mb-3">
104           <div className="card-body">
105             {this.description()}
106             {this.badges()}
107             {this.mods()}
108           </div>
109         </div>
110       </div>
111     );
112   }
113
114   communityTitle() {
115     let community = this.props.community_view.community;
116     let subscribed = this.props.community_view.subscribed;
117     return (
118       <div>
119         <h5 className="mb-0">
120           {this.props.showIcon && (
121             <BannerIconHeader icon={community.icon} banner={community.banner} />
122           )}
123           <span className="mr-2">{community.title}</span>
124           {subscribed == SubscribedType.Subscribed && (
125             <button
126               className="btn btn-secondary btn-sm mr-2"
127               onClick={linkEvent(this, this.handleUnsubscribe)}
128             >
129               <Icon icon="check" classes="icon-inline text-success mr-1" />
130               {i18n.t("joined")}
131             </button>
132           )}
133           {subscribed == SubscribedType.Pending && (
134             <button
135               className="btn btn-warning mr-2"
136               onClick={linkEvent(this, this.handleUnsubscribe)}
137             >
138               {i18n.t("subscribe_pending")}
139             </button>
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 className="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 className="list-inline small">
264         <li className="list-inline-item">{i18n.t("mods")}: </li>
265         {this.props.moderators.map(mod => (
266           <li key={mod.moderator.id} className="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       <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   subscribe() {
289     let community_view = this.props.community_view;
290     return (
291       <div className="mb-2">
292         {community_view.subscribed == SubscribedType.NotSubscribed && (
293           <button
294             className="btn btn-secondary btn-block"
295             onClick={linkEvent(this, this.handleSubscribe)}
296           >
297             {i18n.t("subscribe")}
298           </button>
299         )}
300       </div>
301     );
302   }
303
304   blockCommunity() {
305     let community_view = this.props.community_view;
306     let blocked = this.props.community_view.blocked;
307
308     return (
309       <div className="mb-2">
310         {community_view.subscribed == SubscribedType.NotSubscribed &&
311           (blocked ? (
312             <button
313               className="btn btn-danger btn-block"
314               onClick={linkEvent(this, this.handleUnblock)}
315             >
316               {i18n.t("unblock_community")}
317             </button>
318           ) : (
319             <button
320               className="btn btn-danger btn-block"
321               onClick={linkEvent(this, this.handleBlock)}
322             >
323               {i18n.t("block_community")}
324             </button>
325           ))}
326       </div>
327     );
328   }
329
330   description() {
331     let description = this.props.community_view.community.description;
332     return description.match({
333       some: desc => (
334         <div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} />
335       ),
336       none: <></>,
337     });
338   }
339
340   adminButtons() {
341     let community_view = this.props.community_view;
342     return (
343       <>
344         <ul className="list-inline mb-1 text-muted font-weight-bold">
345           {amMod(Some(this.props.moderators)) && (
346             <>
347               <li className="list-inline-item-action">
348                 <button
349                   className="btn btn-link text-muted d-inline-block"
350                   onClick={linkEvent(this, this.handleEditClick)}
351                   data-tippy-content={i18n.t("edit")}
352                   aria-label={i18n.t("edit")}
353                 >
354                   <Icon icon="edit" classes="icon-inline" />
355                 </button>
356               </li>
357               {!amTopMod(Some(this.props.moderators)) &&
358                 (!this.state.showConfirmLeaveModTeam ? (
359                   <li className="list-inline-item-action">
360                     <button
361                       className="btn btn-link text-muted d-inline-block"
362                       onClick={linkEvent(
363                         this,
364                         this.handleShowConfirmLeaveModTeamClick
365                       )}
366                     >
367                       {i18n.t("leave_mod_team")}
368                     </button>
369                   </li>
370                 ) : (
371                   <>
372                     <li className="list-inline-item-action">
373                       {i18n.t("are_you_sure")}
374                     </li>
375                     <li className="list-inline-item-action">
376                       <button
377                         className="btn btn-link text-muted d-inline-block"
378                         onClick={linkEvent(this, this.handleLeaveModTeamClick)}
379                       >
380                         {i18n.t("yes")}
381                       </button>
382                     </li>
383                     <li className="list-inline-item-action">
384                       <button
385                         className="btn btn-link text-muted d-inline-block"
386                         onClick={linkEvent(
387                           this,
388                           this.handleCancelLeaveModTeamClick
389                         )}
390                       >
391                         {i18n.t("no")}
392                       </button>
393                     </li>
394                   </>
395                 ))}
396               {amTopMod(Some(this.props.moderators)) && (
397                 <li className="list-inline-item-action">
398                   <button
399                     className="btn btn-link text-muted d-inline-block"
400                     onClick={linkEvent(this, this.handleDeleteClick)}
401                     data-tippy-content={
402                       !community_view.community.deleted
403                         ? i18n.t("delete")
404                         : i18n.t("restore")
405                     }
406                     aria-label={
407                       !community_view.community.deleted
408                         ? i18n.t("delete")
409                         : i18n.t("restore")
410                     }
411                   >
412                     <Icon
413                       icon="trash"
414                       classes={`icon-inline ${
415                         community_view.community.deleted && "text-danger"
416                       }`}
417                     />
418                   </button>
419                 </li>
420               )}
421             </>
422           )}
423           {amAdmin(Some(this.props.admins)) && (
424             <li className="list-inline-item">
425               {!this.props.community_view.community.removed ? (
426                 <button
427                   className="btn btn-link text-muted d-inline-block"
428                   onClick={linkEvent(this, this.handleModRemoveShow)}
429                 >
430                   {i18n.t("remove")}
431                 </button>
432               ) : (
433                 <button
434                   className="btn btn-link text-muted d-inline-block"
435                   onClick={linkEvent(this, this.handleModRemoveSubmit)}
436                 >
437                   {i18n.t("restore")}
438                 </button>
439               )}
440               <button
441                 className="btn btn-link text-muted d-inline-block"
442                 onClick={linkEvent(this, this.handlePurgeCommunityShow)}
443                 aria-label={i18n.t("purge_community")}
444               >
445                 {i18n.t("purge_community")}
446               </button>
447             </li>
448           )}
449         </ul>
450         {this.state.showRemoveDialog && (
451           <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
452             <div className="form-group">
453               <label className="col-form-label" htmlFor="remove-reason">
454                 {i18n.t("reason")}
455               </label>
456               <input
457                 type="text"
458                 id="remove-reason"
459                 className="form-control mr-2"
460                 placeholder={i18n.t("optional")}
461                 value={toUndefined(this.state.removeReason)}
462                 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
463               />
464             </div>
465             {/* TODO hold off on expires for now */}
466             {/* <div class="form-group row"> */}
467             {/*   <label class="col-form-label">Expires</label> */}
468             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
469             {/* </div> */}
470             <div className="form-group">
471               <button type="submit" className="btn btn-secondary">
472                 {i18n.t("remove_community")}
473               </button>
474             </div>
475           </form>
476         )}
477         {this.state.showPurgeDialog && (
478           <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
479             <div className="form-group">
480               <PurgeWarning />
481             </div>
482             <div className="form-group">
483               <label className="sr-only" htmlFor="purge-reason">
484                 {i18n.t("reason")}
485               </label>
486               <input
487                 type="text"
488                 id="purge-reason"
489                 className="form-control mr-2"
490                 placeholder={i18n.t("reason")}
491                 value={toUndefined(this.state.purgeReason)}
492                 onInput={linkEvent(this, this.handlePurgeReasonChange)}
493               />
494             </div>
495             <div className="form-group">
496               {this.state.purgeLoading ? (
497                 <Spinner />
498               ) : (
499                 <button
500                   type="submit"
501                   className="btn btn-secondary"
502                   aria-label={i18n.t("purge_community")}
503                 >
504                   {i18n.t("purge_community")}
505                 </button>
506               )}
507             </div>
508           </form>
509         )}
510       </>
511     );
512   }
513
514   handleEditClick(i: Sidebar) {
515     i.setState({ showEdit: true });
516   }
517
518   handleEditCommunity() {
519     this.setState({ showEdit: false });
520   }
521
522   handleEditCancel() {
523     this.setState({ showEdit: false });
524   }
525
526   handleDeleteClick(i: Sidebar, event: any) {
527     event.preventDefault();
528     let deleteForm = new DeleteCommunity({
529       community_id: i.props.community_view.community.id,
530       deleted: !i.props.community_view.community.deleted,
531       auth: auth().unwrap(),
532     });
533     WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm));
534   }
535
536   handleShowConfirmLeaveModTeamClick(i: Sidebar) {
537     i.setState({ showConfirmLeaveModTeam: true });
538   }
539
540   handleLeaveModTeamClick(i: Sidebar) {
541     UserService.Instance.myUserInfo.match({
542       some: mui => {
543         let form = new AddModToCommunity({
544           person_id: mui.local_user_view.person.id,
545           community_id: i.props.community_view.community.id,
546           added: false,
547           auth: auth().unwrap(),
548         });
549         WebSocketService.Instance.send(wsClient.addModToCommunity(form));
550         i.setState({ showConfirmLeaveModTeam: false });
551       },
552       none: void 0,
553     });
554   }
555
556   handleCancelLeaveModTeamClick(i: Sidebar) {
557     i.setState({ showConfirmLeaveModTeam: false });
558   }
559
560   handleUnsubscribe(i: Sidebar, event: any) {
561     event.preventDefault();
562     let community_id = i.props.community_view.community.id;
563     let form = new FollowCommunity({
564       community_id,
565       follow: false,
566       auth: auth().unwrap(),
567     });
568     WebSocketService.Instance.send(wsClient.followCommunity(form));
569
570     // Update myUserInfo
571     UserService.Instance.myUserInfo.match({
572       some: mui =>
573         (mui.follows = mui.follows.filter(i => i.community.id != community_id)),
574       none: void 0,
575     });
576   }
577
578   handleSubscribe(i: Sidebar, event: any) {
579     event.preventDefault();
580     let community_id = i.props.community_view.community.id;
581     let form = new FollowCommunity({
582       community_id,
583       follow: true,
584       auth: auth().unwrap(),
585     });
586     WebSocketService.Instance.send(wsClient.followCommunity(form));
587
588     // Update myUserInfo
589     UserService.Instance.myUserInfo.match({
590       some: mui =>
591         mui.follows.push({
592           community: i.props.community_view.community,
593           follower: mui.local_user_view.person,
594         }),
595       none: void 0,
596     });
597   }
598
599   get canPost(): boolean {
600     return (
601       !this.props.community_view.community.posting_restricted_to_mods ||
602       amMod(Some(this.props.moderators)) ||
603       amAdmin(Some(this.props.admins))
604     );
605   }
606
607   handleModRemoveShow(i: Sidebar) {
608     i.setState({ showRemoveDialog: true });
609   }
610
611   handleModRemoveReasonChange(i: Sidebar, event: any) {
612     i.setState({ removeReason: Some(event.target.value) });
613   }
614
615   handleModRemoveExpiresChange(i: Sidebar, event: any) {
616     i.setState({ removeExpires: Some(event.target.value) });
617   }
618
619   handleModRemoveSubmit(i: Sidebar, event: any) {
620     event.preventDefault();
621     let removeForm = new RemoveCommunity({
622       community_id: i.props.community_view.community.id,
623       removed: !i.props.community_view.community.removed,
624       reason: i.state.removeReason,
625       expires: i.state.removeExpires.map(getUnixTime),
626       auth: auth().unwrap(),
627     });
628     WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
629
630     i.setState({ showRemoveDialog: false });
631   }
632
633   handlePurgeCommunityShow(i: Sidebar) {
634     i.setState({ showPurgeDialog: true, showRemoveDialog: false });
635   }
636
637   handlePurgeReasonChange(i: Sidebar, event: any) {
638     i.setState({ purgeReason: Some(event.target.value) });
639   }
640
641   handlePurgeSubmit(i: Sidebar, event: any) {
642     event.preventDefault();
643
644     let form = new PurgeCommunity({
645       community_id: i.props.community_view.community.id,
646       reason: i.state.purgeReason,
647       auth: auth().unwrap(),
648     });
649     WebSocketService.Instance.send(wsClient.purgeCommunity(form));
650
651     i.setState({ purgeLoading: true });
652   }
653
654   handleBlock(i: Sidebar, event: any) {
655     event.preventDefault();
656     let blockCommunityForm = new BlockCommunity({
657       community_id: i.props.community_view.community.id,
658       block: true,
659       auth: auth().unwrap(),
660     });
661     WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm));
662   }
663
664   handleUnblock(i: Sidebar, event: any) {
665     event.preventDefault();
666     let blockCommunityForm = new BlockCommunity({
667       community_id: i.props.community_view.community.id,
668       block: false,
669       auth: auth().unwrap(),
670     });
671     WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm));
672   }
673 }