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