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