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