]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/sidebar.tsx
feat: Move Badges to common component
[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   online: number;
43   enableNsfw?: boolean;
44   showIcon?: boolean;
45   editable?: boolean;
46   onDeleteCommunity(form: DeleteCommunity): void;
47   onRemoveCommunity(form: RemoveCommunity): void;
48   onLeaveModTeam(form: AddModToCommunity): void;
49   onFollowCommunity(form: FollowCommunity): void;
50   onBlockCommunity(form: BlockCommunity): void;
51   onPurgeCommunity(form: PurgeCommunity): void;
52   onEditCommunity(form: EditCommunity): void;
53 }
54
55 interface SidebarState {
56   removeReason?: string;
57   removeExpires?: string;
58   showEdit: boolean;
59   showRemoveDialog: boolean;
60   showPurgeDialog: boolean;
61   purgeReason?: string;
62   showConfirmLeaveModTeam: boolean;
63   deleteCommunityLoading: boolean;
64   removeCommunityLoading: boolean;
65   leaveModTeamLoading: boolean;
66   followCommunityLoading: boolean;
67   purgeCommunityLoading: boolean;
68 }
69
70 export class Sidebar extends Component<SidebarProps, SidebarState> {
71   state: SidebarState = {
72     showEdit: false,
73     showRemoveDialog: false,
74     showPurgeDialog: false,
75     showConfirmLeaveModTeam: false,
76     deleteCommunityLoading: false,
77     removeCommunityLoading: false,
78     leaveModTeamLoading: false,
79     followCommunityLoading: false,
80     purgeCommunityLoading: false,
81   };
82
83   constructor(props: any, context: any) {
84     super(props, context);
85     this.handleEditCancel = this.handleEditCancel.bind(this);
86   }
87
88   componentWillReceiveProps(
89     nextProps: Readonly<{ children?: InfernoNode } & SidebarProps>
90   ): void {
91     if (this.props.moderators != nextProps.moderators) {
92       this.setState({
93         showConfirmLeaveModTeam: false,
94       });
95     }
96
97     if (this.props.community_view != nextProps.community_view) {
98       this.setState({
99         showEdit: false,
100         showPurgeDialog: false,
101         showRemoveDialog: false,
102         deleteCommunityLoading: false,
103         removeCommunityLoading: false,
104         leaveModTeamLoading: false,
105         followCommunityLoading: false,
106         purgeCommunityLoading: false,
107       });
108     }
109   }
110
111   render() {
112     return (
113       <div>
114         {!this.state.showEdit ? (
115           this.sidebar()
116         ) : (
117           <CommunityForm
118             community_view={this.props.community_view}
119             allLanguages={this.props.allLanguages}
120             siteLanguages={this.props.siteLanguages}
121             communityLanguages={this.props.communityLanguages}
122             onUpsertCommunity={this.props.onEditCommunity}
123             onCancel={this.handleEditCancel}
124             enableNsfw={this.props.enableNsfw}
125           />
126         )}
127       </div>
128     );
129   }
130
131   sidebar() {
132     const myUSerInfo = UserService.Instance.myUserInfo;
133     const { name, actor_id } = this.props.community_view.community;
134     return (
135       <div>
136         <div 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         </div>
158         <div className="card border-secondary mb-3">
159           <div className="card-body">
160             {this.description()}
161             <Badges
162               online={this.props.online}
163               community_view={this.props.community_view}
164               counts={this.props.community_view.counts}
165             />
166             {this.mods()}
167           </div>
168         </div>
169       </div>
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="mr-2">
183             <CommunityLink community={community} hideAvatar />
184           </span>
185           {subscribed === "Subscribed" && (
186             <button
187               className="btn btn-secondary btn-sm mr-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 mr-1" />
195                   {i18n.t("joined")}
196                 </>
197               )}
198             </button>
199           )}
200           {subscribed === "Pending" && (
201             <button
202               className="btn btn-warning mr-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="mr-2 text-muted font-italic">
214               {i18n.t("removed")}
215             </small>
216           )}
217           {community.deleted && (
218             <small className="mr-2 text-muted font-italic">
219               {i18n.t("deleted")}
220             </small>
221           )}
222           {community.nsfw && (
223             <small className="mr-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 btn-block mb-2 ${
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       <div className="mb-2">
270         {community_view.subscribed == "NotSubscribed" && (
271           <button
272             className="btn btn-secondary btn-block"
273             onClick={linkEvent(this, this.handleFollowCommunity)}
274           >
275             {this.state.followCommunityLoading ? (
276               <Spinner />
277             ) : (
278               i18n.t("subscribe")
279             )}
280           </button>
281         )}
282       </div>
283     );
284   }
285
286   blockCommunity() {
287     const { subscribed, blocked } = this.props.community_view;
288
289     return (
290       <div className="mb-2">
291         {subscribed == "NotSubscribed" && (
292           <button
293             className="btn btn-danger btn-block"
294             onClick={linkEvent(this, this.handleBlockCommunity)}
295           >
296             {i18n.t(blocked ? "unblock_community" : "block_community")}
297           </button>
298         )}
299       </div>
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="form-group">
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 mr-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="form-group row"> */}
447             {/*   <label class="col-form-label">Expires</label> */}
448             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
449             {/* </div> */}
450             <div className="form-group">
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="form-group">
464               <PurgeWarning />
465             </div>
466             <div className="form-group">
467               <label className="sr-only" htmlFor="purge-reason">
468                 {i18n.t("reason")}
469               </label>
470               <input
471                 type="text"
472                 id="purge-reason"
473                 className="form-control mr-2"
474                 placeholder={i18n.t("reason")}
475                 value={this.state.purgeReason}
476                 onInput={linkEvent(this, this.handlePurgeReasonChange)}
477               />
478             </div>
479             <div className="form-group">
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 }