]> Untitled Git - lemmy-ui.git/blob - src/shared/components/community/sidebar.tsx
Adding Community Language fixes. #783 (#868)
[lemmy-ui.git] / src / shared / components / community / sidebar.tsx
1 import { None, Option, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
3 import { Link } from "inferno-router";
4 import {
5   AddModToCommunity,
6   BlockCommunity,
7   CommunityModeratorView,
8   CommunityView,
9   DeleteCommunity,
10   FollowCommunity,
11   Language,
12   PersonViewSafe,
13   PurgeCommunity,
14   RemoveCommunity,
15   SubscribedType,
16   toUndefined,
17 } from "lemmy-js-client";
18 import { i18n } from "../../i18next";
19 import { UserService, WebSocketService } from "../../services";
20 import {
21   amAdmin,
22   amMod,
23   amTopMod,
24   auth,
25   getUnixTime,
26   mdToHtml,
27   numToSI,
28   wsClient,
29 } from "../../utils";
30 import { BannerIconHeader } from "../common/banner-icon-header";
31 import { Icon, PurgeWarning, Spinner } from "../common/icon";
32 import { CommunityForm } from "../community/community-form";
33 import { CommunityLink } from "../community/community-link";
34 import { PersonListing } from "../person/person-listing";
35
36 interface SidebarProps {
37   community_view: CommunityView;
38   moderators: CommunityModeratorView[];
39   admins: PersonViewSafe[];
40   allLanguages: Language[];
41   siteLanguages: number[];
42   communityLanguages: Option<number[]>;
43   online: number;
44   enableNsfw?: boolean;
45   showIcon?: boolean;
46   editable?: boolean;
47 }
48
49 interface SidebarState {
50   removeReason: Option<string>;
51   removeExpires: Option<string>;
52   showEdit: boolean;
53   showRemoveDialog: boolean;
54   showPurgeDialog: boolean;
55   purgeReason: Option<string>;
56   purgeLoading: boolean;
57   showConfirmLeaveModTeam: boolean;
58 }
59
60 export class Sidebar extends Component<SidebarProps, SidebarState> {
61   private emptyState: SidebarState = {
62     showEdit: false,
63     showRemoveDialog: false,
64     removeReason: None,
65     removeExpires: None,
66     showPurgeDialog: false,
67     purgeReason: None,
68     purgeLoading: false,
69     showConfirmLeaveModTeam: false,
70   };
71
72   constructor(props: any, context: any) {
73     super(props, context);
74     this.state = this.emptyState;
75     this.handleEditCommunity = this.handleEditCommunity.bind(this);
76     this.handleEditCancel = this.handleEditCancel.bind(this);
77   }
78
79   render() {
80     return (
81       <div>
82         {!this.state.showEdit ? (
83           this.sidebar()
84         ) : (
85           <CommunityForm
86             community_view={Some(this.props.community_view)}
87             allLanguages={this.props.allLanguages}
88             siteLanguages={this.props.siteLanguages}
89             communityLanguages={this.props.communityLanguages}
90             onEdit={this.handleEditCommunity}
91             onCancel={this.handleEditCancel}
92             enableNsfw={this.props.enableNsfw}
93           />
94         )}
95       </div>
96     );
97   }
98
99   sidebar() {
100     return (
101       <div>
102         <div className="card border-secondary mb-3">
103           <div className="card-body">
104             {this.communityTitle()}
105             {this.props.editable && this.adminButtons()}
106             {this.subscribe()}
107             {this.canPost && this.createPost()}
108             {this.blockCommunity()}
109           </div>
110         </div>
111         <div className="card border-secondary mb-3">
112           <div className="card-body">
113             {this.description()}
114             {this.badges()}
115             {this.mods()}
116           </div>
117         </div>
118       </div>
119     );
120   }
121
122   communityTitle() {
123     let community = this.props.community_view.community;
124     let subscribed = this.props.community_view.subscribed;
125     return (
126       <div>
127         <h5 className="mb-0">
128           {this.props.showIcon && (
129             <BannerIconHeader icon={community.icon} banner={community.banner} />
130           )}
131           <span className="mr-2">{community.title}</span>
132           {subscribed == SubscribedType.Subscribed && (
133             <button
134               className="btn btn-secondary btn-sm mr-2"
135               onClick={linkEvent(this, this.handleUnsubscribe)}
136             >
137               <Icon icon="check" classes="icon-inline text-success mr-1" />
138               {i18n.t("joined")}
139             </button>
140           )}
141           {subscribed == SubscribedType.Pending && (
142             <button
143               className="btn btn-warning mr-2"
144               onClick={linkEvent(this, this.handleUnsubscribe)}
145             >
146               {i18n.t("subscribe_pending")}
147             </button>
148           )}
149           {community.removed && (
150             <small className="mr-2 text-muted font-italic">
151               {i18n.t("removed")}
152             </small>
153           )}
154           {community.deleted && (
155             <small className="mr-2 text-muted font-italic">
156               {i18n.t("deleted")}
157             </small>
158           )}
159           {community.nsfw && (
160             <small className="mr-2 text-muted font-italic">
161               {i18n.t("nsfw")}
162             </small>
163           )}
164         </h5>
165         <CommunityLink
166           community={community}
167           realLink
168           useApubName
169           muted
170           hideAvatar
171         />
172       </div>
173     );
174   }
175
176   badges() {
177     let community_view = this.props.community_view;
178     let counts = community_view.counts;
179     return (
180       <ul className="my-1 list-inline">
181         <li className="list-inline-item badge badge-secondary">
182           {i18n.t("number_online", {
183             count: this.props.online,
184             formattedCount: numToSI(this.props.online),
185           })}
186         </li>
187         <li
188           className="list-inline-item badge badge-secondary pointer"
189           data-tippy-content={i18n.t("active_users_in_the_last_day", {
190             count: counts.users_active_day,
191             formattedCount: counts.users_active_day,
192           })}
193         >
194           {i18n.t("number_of_users", {
195             count: counts.users_active_day,
196             formattedCount: numToSI(counts.users_active_day),
197           })}{" "}
198           / {i18n.t("day")}
199         </li>
200         <li
201           className="list-inline-item badge badge-secondary pointer"
202           data-tippy-content={i18n.t("active_users_in_the_last_week", {
203             count: counts.users_active_week,
204             formattedCount: counts.users_active_week,
205           })}
206         >
207           {i18n.t("number_of_users", {
208             count: counts.users_active_week,
209             formattedCount: numToSI(counts.users_active_week),
210           })}{" "}
211           / {i18n.t("week")}
212         </li>
213         <li
214           className="list-inline-item badge badge-secondary pointer"
215           data-tippy-content={i18n.t("active_users_in_the_last_month", {
216             count: counts.users_active_month,
217             formattedCount: counts.users_active_month,
218           })}
219         >
220           {i18n.t("number_of_users", {
221             count: counts.users_active_month,
222             formattedCount: numToSI(counts.users_active_month),
223           })}{" "}
224           / {i18n.t("month")}
225         </li>
226         <li
227           className="list-inline-item badge badge-secondary pointer"
228           data-tippy-content={i18n.t("active_users_in_the_last_six_months", {
229             count: counts.users_active_half_year,
230             formattedCount: counts.users_active_half_year,
231           })}
232         >
233           {i18n.t("number_of_users", {
234             count: counts.users_active_half_year,
235             formattedCount: numToSI(counts.users_active_half_year),
236           })}{" "}
237           / {i18n.t("number_of_months", { count: 6, formattedCount: 6 })}
238         </li>
239         <li className="list-inline-item badge badge-secondary">
240           {i18n.t("number_of_subscribers", {
241             count: counts.subscribers,
242             formattedCount: numToSI(counts.subscribers),
243           })}
244         </li>
245         <li className="list-inline-item badge badge-secondary">
246           {i18n.t("number_of_posts", {
247             count: counts.posts,
248             formattedCount: numToSI(counts.posts),
249           })}
250         </li>
251         <li className="list-inline-item badge badge-secondary">
252           {i18n.t("number_of_comments", {
253             count: counts.comments,
254             formattedCount: numToSI(counts.comments),
255           })}
256         </li>
257         <li className="list-inline-item">
258           <Link
259             className="badge badge-primary"
260             to={`/modlog/community/${this.props.community_view.community.id}`}
261           >
262             {i18n.t("modlog")}
263           </Link>
264         </li>
265       </ul>
266     );
267   }
268
269   mods() {
270     return (
271       <ul className="list-inline small">
272         <li className="list-inline-item">{i18n.t("mods")}: </li>
273         {this.props.moderators.map(mod => (
274           <li key={mod.moderator.id} className="list-inline-item">
275             <PersonListing person={mod.moderator} />
276           </li>
277         ))}
278       </ul>
279     );
280   }
281
282   createPost() {
283     let cv = this.props.community_view;
284     return (
285       <Link
286         className={`btn btn-secondary btn-block mb-2 ${
287           cv.community.deleted || cv.community.removed ? "no-click" : ""
288         }`}
289         to={`/create_post?community_id=${cv.community.id}`}
290       >
291         {i18n.t("create_a_post")}
292       </Link>
293     );
294   }
295
296   subscribe() {
297     let community_view = this.props.community_view;
298     return (
299       <div className="mb-2">
300         {community_view.subscribed == SubscribedType.NotSubscribed && (
301           <button
302             className="btn btn-secondary btn-block"
303             onClick={linkEvent(this, this.handleSubscribe)}
304           >
305             {i18n.t("subscribe")}
306           </button>
307         )}
308       </div>
309     );
310   }
311
312   blockCommunity() {
313     let community_view = this.props.community_view;
314     let blocked = this.props.community_view.blocked;
315
316     return (
317       <div className="mb-2">
318         {community_view.subscribed == SubscribedType.NotSubscribed &&
319           (blocked ? (
320             <button
321               className="btn btn-danger btn-block"
322               onClick={linkEvent(this, this.handleUnblock)}
323             >
324               {i18n.t("unblock_community")}
325             </button>
326           ) : (
327             <button
328               className="btn btn-danger btn-block"
329               onClick={linkEvent(this, this.handleBlock)}
330             >
331               {i18n.t("block_community")}
332             </button>
333           ))}
334       </div>
335     );
336   }
337
338   description() {
339     let description = this.props.community_view.community.description;
340     return description.match({
341       some: desc => (
342         <div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} />
343       ),
344       none: <></>,
345     });
346   }
347
348   adminButtons() {
349     let community_view = this.props.community_view;
350     return (
351       <>
352         <ul className="list-inline mb-1 text-muted font-weight-bold">
353           {amMod(Some(this.props.moderators)) && (
354             <>
355               <li className="list-inline-item-action">
356                 <button
357                   className="btn btn-link text-muted d-inline-block"
358                   onClick={linkEvent(this, this.handleEditClick)}
359                   data-tippy-content={i18n.t("edit")}
360                   aria-label={i18n.t("edit")}
361                 >
362                   <Icon icon="edit" classes="icon-inline" />
363                 </button>
364               </li>
365               {!amTopMod(Some(this.props.moderators)) &&
366                 (!this.state.showConfirmLeaveModTeam ? (
367                   <li className="list-inline-item-action">
368                     <button
369                       className="btn btn-link text-muted d-inline-block"
370                       onClick={linkEvent(
371                         this,
372                         this.handleShowConfirmLeaveModTeamClick
373                       )}
374                     >
375                       {i18n.t("leave_mod_team")}
376                     </button>
377                   </li>
378                 ) : (
379                   <>
380                     <li className="list-inline-item-action">
381                       {i18n.t("are_you_sure")}
382                     </li>
383                     <li className="list-inline-item-action">
384                       <button
385                         className="btn btn-link text-muted d-inline-block"
386                         onClick={linkEvent(this, this.handleLeaveModTeamClick)}
387                       >
388                         {i18n.t("yes")}
389                       </button>
390                     </li>
391                     <li className="list-inline-item-action">
392                       <button
393                         className="btn btn-link text-muted d-inline-block"
394                         onClick={linkEvent(
395                           this,
396                           this.handleCancelLeaveModTeamClick
397                         )}
398                       >
399                         {i18n.t("no")}
400                       </button>
401                     </li>
402                   </>
403                 ))}
404               {amTopMod(Some(this.props.moderators)) && (
405                 <li className="list-inline-item-action">
406                   <button
407                     className="btn btn-link text-muted d-inline-block"
408                     onClick={linkEvent(this, this.handleDeleteClick)}
409                     data-tippy-content={
410                       !community_view.community.deleted
411                         ? i18n.t("delete")
412                         : i18n.t("restore")
413                     }
414                     aria-label={
415                       !community_view.community.deleted
416                         ? i18n.t("delete")
417                         : i18n.t("restore")
418                     }
419                   >
420                     <Icon
421                       icon="trash"
422                       classes={`icon-inline ${
423                         community_view.community.deleted && "text-danger"
424                       }`}
425                     />
426                   </button>
427                 </li>
428               )}
429             </>
430           )}
431           {amAdmin() && (
432             <li className="list-inline-item">
433               {!this.props.community_view.community.removed ? (
434                 <button
435                   className="btn btn-link text-muted d-inline-block"
436                   onClick={linkEvent(this, this.handleModRemoveShow)}
437                 >
438                   {i18n.t("remove")}
439                 </button>
440               ) : (
441                 <button
442                   className="btn btn-link text-muted d-inline-block"
443                   onClick={linkEvent(this, this.handleModRemoveSubmit)}
444                 >
445                   {i18n.t("restore")}
446                 </button>
447               )}
448               <button
449                 className="btn btn-link text-muted d-inline-block"
450                 onClick={linkEvent(this, this.handlePurgeCommunityShow)}
451                 aria-label={i18n.t("purge_community")}
452               >
453                 {i18n.t("purge_community")}
454               </button>
455             </li>
456           )}
457         </ul>
458         {this.state.showRemoveDialog && (
459           <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
460             <div className="form-group">
461               <label className="col-form-label" htmlFor="remove-reason">
462                 {i18n.t("reason")}
463               </label>
464               <input
465                 type="text"
466                 id="remove-reason"
467                 className="form-control mr-2"
468                 placeholder={i18n.t("optional")}
469                 value={toUndefined(this.state.removeReason)}
470                 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
471               />
472             </div>
473             {/* TODO hold off on expires for now */}
474             {/* <div class="form-group row"> */}
475             {/*   <label class="col-form-label">Expires</label> */}
476             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
477             {/* </div> */}
478             <div className="form-group">
479               <button type="submit" className="btn btn-secondary">
480                 {i18n.t("remove_community")}
481               </button>
482             </div>
483           </form>
484         )}
485         {this.state.showPurgeDialog && (
486           <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
487             <div className="form-group">
488               <PurgeWarning />
489             </div>
490             <div className="form-group">
491               <label className="sr-only" htmlFor="purge-reason">
492                 {i18n.t("reason")}
493               </label>
494               <input
495                 type="text"
496                 id="purge-reason"
497                 className="form-control mr-2"
498                 placeholder={i18n.t("reason")}
499                 value={toUndefined(this.state.purgeReason)}
500                 onInput={linkEvent(this, this.handlePurgeReasonChange)}
501               />
502             </div>
503             <div className="form-group">
504               {this.state.purgeLoading ? (
505                 <Spinner />
506               ) : (
507                 <button
508                   type="submit"
509                   className="btn btn-secondary"
510                   aria-label={i18n.t("purge_community")}
511                 >
512                   {i18n.t("purge_community")}
513                 </button>
514               )}
515             </div>
516           </form>
517         )}
518       </>
519     );
520   }
521
522   handleEditClick(i: Sidebar) {
523     i.setState({ showEdit: true });
524   }
525
526   handleEditCommunity() {
527     this.setState({ showEdit: false });
528   }
529
530   handleEditCancel() {
531     this.setState({ showEdit: false });
532   }
533
534   handleDeleteClick(i: Sidebar, event: any) {
535     event.preventDefault();
536     let deleteForm = new DeleteCommunity({
537       community_id: i.props.community_view.community.id,
538       deleted: !i.props.community_view.community.deleted,
539       auth: auth().unwrap(),
540     });
541     WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm));
542   }
543
544   handleShowConfirmLeaveModTeamClick(i: Sidebar) {
545     i.setState({ showConfirmLeaveModTeam: true });
546   }
547
548   handleLeaveModTeamClick(i: Sidebar) {
549     UserService.Instance.myUserInfo.match({
550       some: mui => {
551         let form = new AddModToCommunity({
552           person_id: mui.local_user_view.person.id,
553           community_id: i.props.community_view.community.id,
554           added: false,
555           auth: auth().unwrap(),
556         });
557         WebSocketService.Instance.send(wsClient.addModToCommunity(form));
558         i.setState({ showConfirmLeaveModTeam: false });
559       },
560       none: void 0,
561     });
562   }
563
564   handleCancelLeaveModTeamClick(i: Sidebar) {
565     i.setState({ showConfirmLeaveModTeam: false });
566   }
567
568   handleUnsubscribe(i: Sidebar, event: any) {
569     event.preventDefault();
570     let community_id = i.props.community_view.community.id;
571     let form = new FollowCommunity({
572       community_id,
573       follow: false,
574       auth: auth().unwrap(),
575     });
576     WebSocketService.Instance.send(wsClient.followCommunity(form));
577
578     // Update myUserInfo
579     UserService.Instance.myUserInfo.match({
580       some: mui =>
581         (mui.follows = mui.follows.filter(i => i.community.id != community_id)),
582       none: void 0,
583     });
584   }
585
586   handleSubscribe(i: Sidebar, event: any) {
587     event.preventDefault();
588     let community_id = i.props.community_view.community.id;
589     let form = new FollowCommunity({
590       community_id,
591       follow: true,
592       auth: auth().unwrap(),
593     });
594     WebSocketService.Instance.send(wsClient.followCommunity(form));
595
596     // Update myUserInfo
597     UserService.Instance.myUserInfo.match({
598       some: mui =>
599         mui.follows.push({
600           community: i.props.community_view.community,
601           follower: mui.local_user_view.person,
602         }),
603       none: void 0,
604     });
605   }
606
607   get canPost(): boolean {
608     return (
609       !this.props.community_view.community.posting_restricted_to_mods ||
610       amMod(Some(this.props.moderators)) ||
611       amAdmin()
612     );
613   }
614
615   handleModRemoveShow(i: Sidebar) {
616     i.setState({ showRemoveDialog: true });
617   }
618
619   handleModRemoveReasonChange(i: Sidebar, event: any) {
620     i.setState({ removeReason: Some(event.target.value) });
621   }
622
623   handleModRemoveExpiresChange(i: Sidebar, event: any) {
624     i.setState({ removeExpires: Some(event.target.value) });
625   }
626
627   handleModRemoveSubmit(i: Sidebar, event: any) {
628     event.preventDefault();
629     let removeForm = new RemoveCommunity({
630       community_id: i.props.community_view.community.id,
631       removed: !i.props.community_view.community.removed,
632       reason: i.state.removeReason,
633       expires: i.state.removeExpires.map(getUnixTime),
634       auth: auth().unwrap(),
635     });
636     WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
637
638     i.setState({ showRemoveDialog: false });
639   }
640
641   handlePurgeCommunityShow(i: Sidebar) {
642     i.setState({ showPurgeDialog: true, showRemoveDialog: false });
643   }
644
645   handlePurgeReasonChange(i: Sidebar, event: any) {
646     i.setState({ purgeReason: Some(event.target.value) });
647   }
648
649   handlePurgeSubmit(i: Sidebar, event: any) {
650     event.preventDefault();
651
652     let form = new PurgeCommunity({
653       community_id: i.props.community_view.community.id,
654       reason: i.state.purgeReason,
655       auth: auth().unwrap(),
656     });
657     WebSocketService.Instance.send(wsClient.purgeCommunity(form));
658
659     i.setState({ purgeLoading: true });
660   }
661
662   handleBlock(i: Sidebar, event: any) {
663     event.preventDefault();
664     let blockCommunityForm = new BlockCommunity({
665       community_id: i.props.community_view.community.id,
666       block: true,
667       auth: auth().unwrap(),
668     });
669     WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm));
670   }
671
672   handleUnblock(i: Sidebar, event: any) {
673     event.preventDefault();
674     let blockCommunityForm = new BlockCommunity({
675       community_id: i.props.community_view.community.id,
676       block: false,
677       auth: auth().unwrap(),
678     });
679     WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm));
680   }
681 }