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