]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/sidebar.tsx
81a5dafc7a492a5369bb7d362c2ab927ea63b2d2
[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     const subscribed = this.props.community_view.subscribed;
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           {subscribed === "Subscribed" && (
180             <button
181               className="btn btn-secondary btn-sm me-2"
182               onClick={linkEvent(this, this.handleUnfollowCommunity)}
183             >
184               {this.state.followCommunityLoading ? (
185                 <Spinner />
186               ) : (
187                 <>
188                   <Icon icon="check" classes="icon-inline text-success me-1" />
189                   {I18NextService.i18n.t("joined")}
190                 </>
191               )}
192             </button>
193           )}
194           {subscribed === "Pending" && (
195             <button
196               className="btn btn-warning me-2"
197               onClick={linkEvent(this, this.handleUnfollowCommunity)}
198             >
199               {this.state.followCommunityLoading ? (
200                 <Spinner />
201               ) : (
202                 I18NextService.i18n.t("subscribe_pending")
203               )}
204             </button>
205           )}
206           {community.removed && (
207             <small className="me-2 text-muted font-italic">
208               {I18NextService.i18n.t("removed")}
209             </small>
210           )}
211           {community.deleted && (
212             <small className="me-2 text-muted font-italic">
213               {I18NextService.i18n.t("deleted")}
214             </small>
215           )}
216           {community.nsfw && (
217             <small className="me-2 text-muted font-italic">
218               {I18NextService.i18n.t("nsfw")}
219             </small>
220           )}
221         </h5>
222         <CommunityLink
223           community={community}
224           realLink
225           useApubName
226           muted
227           hideAvatar
228         />
229       </div>
230     );
231   }
232
233   mods() {
234     return (
235       <ul className="list-inline small">
236         <li className="list-inline-item">{I18NextService.i18n.t("mods")}: </li>
237         {this.props.moderators.map(mod => (
238           <li key={mod.moderator.id} className="list-inline-item">
239             <PersonListing person={mod.moderator} />
240           </li>
241         ))}
242       </ul>
243     );
244   }
245
246   createPost() {
247     const cv = this.props.community_view;
248     return (
249       <Link
250         className={`btn btn-secondary d-block mb-2 w-100 ${
251           cv.community.deleted || cv.community.removed ? "no-click" : ""
252         }`}
253         to={`/create_post?communityId=${cv.community.id}`}
254       >
255         {I18NextService.i18n.t("create_a_post")}
256       </Link>
257     );
258   }
259
260   subscribe() {
261     const community_view = this.props.community_view;
262     return (
263       <>
264         {community_view.subscribed == "NotSubscribed" && (
265           <button
266             className="btn btn-secondary d-block mb-2 w-100"
267             onClick={linkEvent(this, this.handleFollowCommunity)}
268           >
269             {this.state.followCommunityLoading ? (
270               <Spinner />
271             ) : (
272               I18NextService.i18n.t("subscribe")
273             )}
274           </button>
275         )}
276       </>
277     );
278   }
279
280   blockCommunity() {
281     const { subscribed, blocked } = this.props.community_view;
282
283     return (
284       <>
285         {subscribed == "NotSubscribed" && (
286           <button
287             className="btn btn-danger d-block mb-2 w-100"
288             onClick={linkEvent(this, this.handleBlockCommunity)}
289           >
290             {I18NextService.i18n.t(
291               blocked ? "unblock_community" : "block_community"
292             )}
293           </button>
294         )}
295       </>
296     );
297   }
298
299   description() {
300     const desc = this.props.community_view.community.description;
301     return (
302       desc && (
303         <div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} />
304       )
305     );
306   }
307
308   adminButtons() {
309     const community_view = this.props.community_view;
310     return (
311       <>
312         <ul className="list-inline mb-1 text-muted font-weight-bold">
313           {amMod(this.props.moderators) && (
314             <>
315               <li className="list-inline-item-action">
316                 <button
317                   className="btn btn-link text-muted d-inline-block"
318                   onClick={linkEvent(this, this.handleEditClick)}
319                   data-tippy-content={I18NextService.i18n.t("edit")}
320                   aria-label={I18NextService.i18n.t("edit")}
321                 >
322                   <Icon icon="edit" classes="icon-inline" />
323                 </button>
324               </li>
325               {!amTopMod(this.props.moderators) &&
326                 (!this.state.showConfirmLeaveModTeam ? (
327                   <li className="list-inline-item-action">
328                     <button
329                       className="btn btn-link text-muted d-inline-block"
330                       onClick={linkEvent(
331                         this,
332                         this.handleShowConfirmLeaveModTeamClick
333                       )}
334                     >
335                       {I18NextService.i18n.t("leave_mod_team")}
336                     </button>
337                   </li>
338                 ) : (
339                   <>
340                     <li className="list-inline-item-action">
341                       {I18NextService.i18n.t("are_you_sure")}
342                     </li>
343                     <li className="list-inline-item-action">
344                       <button
345                         className="btn btn-link text-muted d-inline-block"
346                         onClick={linkEvent(this, this.handleLeaveModTeam)}
347                       >
348                         {I18NextService.i18n.t("yes")}
349                       </button>
350                     </li>
351                     <li className="list-inline-item-action">
352                       <button
353                         className="btn btn-link text-muted d-inline-block"
354                         onClick={linkEvent(
355                           this,
356                           this.handleCancelLeaveModTeamClick
357                         )}
358                       >
359                         {I18NextService.i18n.t("no")}
360                       </button>
361                     </li>
362                   </>
363                 ))}
364               {amTopMod(this.props.moderators) && (
365                 <li className="list-inline-item-action">
366                   <button
367                     className="btn btn-link text-muted d-inline-block"
368                     onClick={linkEvent(this, this.handleDeleteCommunity)}
369                     data-tippy-content={
370                       !community_view.community.deleted
371                         ? I18NextService.i18n.t("delete")
372                         : I18NextService.i18n.t("restore")
373                     }
374                     aria-label={
375                       !community_view.community.deleted
376                         ? I18NextService.i18n.t("delete")
377                         : I18NextService.i18n.t("restore")
378                     }
379                   >
380                     {this.state.deleteCommunityLoading ? (
381                       <Spinner />
382                     ) : (
383                       <Icon
384                         icon="trash"
385                         classes={`icon-inline ${
386                           community_view.community.deleted && "text-danger"
387                         }`}
388                       />
389                     )}{" "}
390                   </button>
391                 </li>
392               )}
393             </>
394           )}
395           {amAdmin() && (
396             <li className="list-inline-item">
397               {!this.props.community_view.community.removed ? (
398                 <button
399                   className="btn btn-link text-muted d-inline-block"
400                   onClick={linkEvent(this, this.handleModRemoveShow)}
401                 >
402                   {I18NextService.i18n.t("remove")}
403                 </button>
404               ) : (
405                 <button
406                   className="btn btn-link text-muted d-inline-block"
407                   onClick={linkEvent(this, this.handleRemoveCommunity)}
408                 >
409                   {this.state.removeCommunityLoading ? (
410                     <Spinner />
411                   ) : (
412                     I18NextService.i18n.t("restore")
413                   )}
414                 </button>
415               )}
416               <button
417                 className="btn btn-link text-muted d-inline-block"
418                 onClick={linkEvent(this, this.handlePurgeCommunityShow)}
419                 aria-label={I18NextService.i18n.t("purge_community")}
420               >
421                 {I18NextService.i18n.t("purge_community")}
422               </button>
423             </li>
424           )}
425         </ul>
426         {this.state.showRemoveDialog && (
427           <form onSubmit={linkEvent(this, this.handleRemoveCommunity)}>
428             <div className="input-group mb-3">
429               <label className="col-form-label" htmlFor="remove-reason">
430                 {I18NextService.i18n.t("reason")}
431               </label>
432               <input
433                 type="text"
434                 id="remove-reason"
435                 className="form-control me-2"
436                 placeholder={I18NextService.i18n.t("optional")}
437                 value={this.state.removeReason}
438                 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
439               />
440             </div>
441             {/* TODO hold off on expires for now */}
442             {/* <div class="mb-3 row"> */}
443             {/*   <label class="col-form-label">Expires</label> */}
444             {/*   <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
445             {/* </div> */}
446             <div className="input-group mb-3">
447               <button type="submit" className="btn btn-secondary">
448                 {this.state.removeCommunityLoading ? (
449                   <Spinner />
450                 ) : (
451                   I18NextService.i18n.t("remove_community")
452                 )}
453               </button>
454             </div>
455           </form>
456         )}
457         {this.state.showPurgeDialog && (
458           <form onSubmit={linkEvent(this, this.handlePurgeCommunity)}>
459             <div className="input-group mb-3">
460               <PurgeWarning />
461             </div>
462             <div className="input-group mb-3">
463               <label className="visually-hidden" htmlFor="purge-reason">
464                 {I18NextService.i18n.t("reason")}
465               </label>
466               <input
467                 type="text"
468                 id="purge-reason"
469                 className="form-control me-2"
470                 placeholder={I18NextService.i18n.t("reason")}
471                 value={this.state.purgeReason}
472                 onInput={linkEvent(this, this.handlePurgeReasonChange)}
473               />
474             </div>
475             <div className="input-group mb-3">
476               {this.state.purgeCommunityLoading ? (
477                 <Spinner />
478               ) : (
479                 <button
480                   type="submit"
481                   className="btn btn-secondary"
482                   aria-label={I18NextService.i18n.t("purge_community")}
483                 >
484                   {I18NextService.i18n.t("purge_community")}
485                 </button>
486               )}
487             </div>
488           </form>
489         )}
490       </>
491     );
492   }
493
494   handleEditClick(i: Sidebar) {
495     i.setState({ showEdit: true });
496   }
497
498   handleEditCancel() {
499     this.setState({ showEdit: false });
500   }
501
502   handleShowConfirmLeaveModTeamClick(i: Sidebar) {
503     i.setState({ showConfirmLeaveModTeam: true });
504   }
505
506   handleCancelLeaveModTeamClick(i: Sidebar) {
507     i.setState({ showConfirmLeaveModTeam: false });
508   }
509
510   get canPost(): boolean {
511     return (
512       !this.props.community_view.community.posting_restricted_to_mods ||
513       amMod(this.props.moderators) ||
514       amAdmin()
515     );
516   }
517
518   handleModRemoveShow(i: Sidebar) {
519     i.setState({ showRemoveDialog: true });
520   }
521
522   handleModRemoveReasonChange(i: Sidebar, event: any) {
523     i.setState({ removeReason: event.target.value });
524   }
525
526   handleModRemoveExpiresChange(i: Sidebar, event: any) {
527     i.setState({ removeExpires: event.target.value });
528   }
529
530   handlePurgeCommunityShow(i: Sidebar) {
531     i.setState({ showPurgeDialog: true, showRemoveDialog: false });
532   }
533
534   handlePurgeReasonChange(i: Sidebar, event: any) {
535     i.setState({ purgeReason: event.target.value });
536   }
537
538   // TODO Do we need two of these?
539   handleUnfollowCommunity(i: Sidebar) {
540     i.setState({ followCommunityLoading: true });
541     i.props.onFollowCommunity({
542       community_id: i.props.community_view.community.id,
543       follow: false,
544       auth: myAuthRequired(),
545     });
546   }
547
548   handleFollowCommunity(i: Sidebar) {
549     i.setState({ followCommunityLoading: true });
550     i.props.onFollowCommunity({
551       community_id: i.props.community_view.community.id,
552       follow: true,
553       auth: myAuthRequired(),
554     });
555   }
556
557   handleBlockCommunity(i: Sidebar) {
558     const { community, blocked } = i.props.community_view;
559
560     i.props.onBlockCommunity({
561       community_id: community.id,
562       block: !blocked,
563       auth: myAuthRequired(),
564     });
565   }
566
567   handleLeaveModTeam(i: Sidebar) {
568     const myId = UserService.Instance.myUserInfo?.local_user_view.person.id;
569     if (myId) {
570       i.setState({ leaveModTeamLoading: true });
571       i.props.onLeaveModTeam({
572         community_id: i.props.community_view.community.id,
573         person_id: 92,
574         added: false,
575         auth: myAuthRequired(),
576       });
577     }
578   }
579
580   handleDeleteCommunity(i: Sidebar) {
581     i.setState({ deleteCommunityLoading: true });
582     i.props.onDeleteCommunity({
583       community_id: i.props.community_view.community.id,
584       deleted: !i.props.community_view.community.deleted,
585       auth: myAuthRequired(),
586     });
587   }
588
589   handleRemoveCommunity(i: Sidebar, event: any) {
590     event.preventDefault();
591     i.setState({ removeCommunityLoading: true });
592     i.props.onRemoveCommunity({
593       community_id: i.props.community_view.community.id,
594       removed: !i.props.community_view.community.removed,
595       reason: i.state.removeReason,
596       expires: getUnixTime(i.state.removeExpires), // TODO fix this
597       auth: myAuthRequired(),
598     });
599   }
600
601   handlePurgeCommunity(i: Sidebar, event: any) {
602     event.preventDefault();
603     i.setState({ purgeCommunityLoading: true });
604     i.props.onPurgeCommunity({
605       community_id: i.props.community_view.community.id,
606       reason: i.state.purgeReason,
607       auth: myAuthRequired(),
608     });
609   }
610 }