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