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