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