]> Untitled Git - lemmy-ui.git/blob - src/shared/components/sidebar.tsx
Add aria attributes where possible (#156)
[lemmy-ui.git] / src / shared / components / sidebar.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import {
4   CommunityView,
5   CommunityModeratorView,
6   FollowCommunity,
7   DeleteCommunity,
8   RemoveCommunity,
9   UserViewSafe,
10   AddModToCommunity,
11   Category,
12 } from 'lemmy-js-client';
13 import { WebSocketService, UserService } from '../services';
14 import { mdToHtml, getUnixTime, wsClient, authField } from '../utils';
15 import { CommunityForm } from './community-form';
16 import { UserListing } from './user-listing';
17 import { CommunityLink } from './community-link';
18 import { BannerIconHeader } from './banner-icon-header';
19 import { i18n } from '../i18next';
20
21 interface SidebarProps {
22   community_view: CommunityView;
23   categories: Category[];
24   moderators: CommunityModeratorView[];
25   admins: UserViewSafe[];
26   online: number;
27   enableNsfw: boolean;
28   showIcon?: boolean;
29 }
30
31 interface SidebarState {
32   showEdit: boolean;
33   showRemoveDialog: boolean;
34   removeReason: string;
35   removeExpires: string;
36   showConfirmLeaveModTeam: boolean;
37 }
38
39 export class Sidebar extends Component<SidebarProps, SidebarState> {
40   private emptyState: SidebarState = {
41     showEdit: false,
42     showRemoveDialog: false,
43     removeReason: null,
44     removeExpires: null,
45     showConfirmLeaveModTeam: false,
46   };
47
48   constructor(props: any, context: any) {
49     super(props, context);
50     this.state = this.emptyState;
51     this.handleEditCommunity = this.handleEditCommunity.bind(this);
52     this.handleEditCancel = this.handleEditCancel.bind(this);
53   }
54
55   render() {
56     return (
57       <div>
58         {!this.state.showEdit ? (
59           this.sidebar()
60         ) : (
61           <CommunityForm
62             categories={this.props.categories}
63             community_view={this.props.community_view}
64             onEdit={this.handleEditCommunity}
65             onCancel={this.handleEditCancel}
66             enableNsfw={this.props.enableNsfw}
67           />
68         )}
69       </div>
70     );
71   }
72
73   sidebar() {
74     return (
75       <div>
76         <div class="card border-secondary mb-3">
77           <div class="card-body">
78             {this.communityTitle()}
79             {this.adminButtons()}
80             {this.subscribe()}
81             {this.createPost()}
82           </div>
83         </div>
84         <div class="card border-secondary mb-3">
85           <div class="card-body">
86             {this.description()}
87             {this.badges()}
88             {this.mods()}
89           </div>
90         </div>
91       </div>
92     );
93   }
94
95   communityTitle() {
96     let community = this.props.community_view.community;
97     let subscribed = this.props.community_view.subscribed;
98     return (
99       <div>
100         <h5 className="mb-0">
101           {this.props.showIcon && (
102             <BannerIconHeader icon={community.icon} banner={community.banner} />
103           )}
104           <span class="mr-2">{community.title}</span>
105           {subscribed && (
106             <a
107               class="btn btn-secondary btn-sm mr-2"
108               href="#"
109               onClick={linkEvent(community.id, this.handleUnsubscribe)}
110             >
111               <svg class="text-success mr-1 icon icon-inline">
112                 <use xlinkHref="#icon-check"></use>
113               </svg>
114               {i18n.t('joined')}
115             </a>
116           )}
117           {community.removed && (
118             <small className="mr-2 text-muted font-italic">
119               {i18n.t('removed')}
120             </small>
121           )}
122           {community.deleted && (
123             <small className="mr-2 text-muted font-italic">
124               {i18n.t('deleted')}
125             </small>
126           )}
127           {community.nsfw && (
128             <small className="mr-2 text-muted font-italic">
129               {i18n.t('nsfw')}
130             </small>
131           )}
132         </h5>
133         <CommunityLink
134           community={community}
135           realLink
136           useApubName
137           muted
138           hideAvatar
139         />
140       </div>
141     );
142   }
143
144   badges() {
145     let community_view = this.props.community_view;
146     let counts = community_view.counts;
147     return (
148       <ul class="my-1 list-inline">
149         <li className="list-inline-item badge badge-secondary">
150           {i18n.t('number_online', { count: this.props.online })}
151         </li>
152         <li
153           className="list-inline-item badge badge-secondary pointer"
154           data-tippy-content={`${i18n.t('number_of_users', {
155             count: counts.users_active_day,
156           })} ${i18n.t('active_in_the_last')} ${i18n.t('day')}`}
157         >
158           {i18n.t('number_of_users', {
159             count: counts.users_active_day,
160           })}{' '}
161           / {i18n.t('day')}
162         </li>
163         <li
164           className="list-inline-item badge badge-secondary pointer"
165           data-tippy-content={`${i18n.t('number_of_users', {
166             count: counts.users_active_week,
167           })} ${i18n.t('active_in_the_last')} ${i18n.t('week')}`}
168         >
169           {i18n.t('number_of_users', {
170             count: counts.users_active_week,
171           })}{' '}
172           / {i18n.t('week')}
173         </li>
174         <li
175           className="list-inline-item badge badge-secondary pointer"
176           data-tippy-content={`${i18n.t('number_of_users', {
177             count: counts.users_active_month,
178           })} ${i18n.t('active_in_the_last')} ${i18n.t('month')}`}
179         >
180           {i18n.t('number_of_users', {
181             count: counts.users_active_month,
182           })}{' '}
183           / {i18n.t('month')}
184         </li>
185         <li
186           className="list-inline-item badge badge-secondary pointer"
187           data-tippy-content={`${i18n.t('number_of_users', {
188             count: counts.users_active_half_year,
189           })} ${i18n.t('active_in_the_last')} ${i18n.t('number_of_months', {
190             count: 6,
191           })}`}
192         >
193           {i18n.t('number_of_users', {
194             count: counts.users_active_half_year,
195           })}{' '}
196           / {i18n.t('number_of_months', { count: 6 })}
197         </li>
198         <li className="list-inline-item badge badge-secondary">
199           {i18n.t('number_of_subscribers', {
200             count: counts.subscribers,
201           })}
202         </li>
203         <li className="list-inline-item badge badge-secondary">
204           {i18n.t('number_of_posts', {
205             count: counts.posts,
206           })}
207         </li>
208         <li className="list-inline-item badge badge-secondary">
209           {i18n.t('number_of_comments', {
210             count: counts.comments,
211           })}
212         </li>
213         <li className="list-inline-item">
214           <Link className="badge badge-secondary" to="/communities">
215             {community_view.category.name}
216           </Link>
217         </li>
218         <li className="list-inline-item">
219           <Link
220             className="badge badge-secondary"
221             to={`/modlog/community/${this.props.community_view.community.id}`}
222           >
223             {i18n.t('modlog')}
224           </Link>
225         </li>
226       </ul>
227     );
228   }
229
230   mods() {
231     return (
232       <ul class="list-inline small">
233         <li class="list-inline-item">{i18n.t('mods')}: </li>
234         {this.props.moderators.map(mod => (
235           <li class="list-inline-item">
236             <UserListing user={mod.moderator} />
237           </li>
238         ))}
239       </ul>
240     );
241   }
242
243   createPost() {
244     let community_view = this.props.community_view;
245     return (
246       community_view.subscribed && (
247         <Link
248           className={`btn btn-secondary btn-block mb-2 ${
249             community_view.community.deleted || community_view.community.removed
250               ? 'no-click'
251               : ''
252           }`}
253           to={`/create_post?community_id=${community_view.community.id}`}
254         >
255           {i18n.t('create_a_post')}
256         </Link>
257       )
258     );
259   }
260
261   subscribe() {
262     let community_view = this.props.community_view;
263     return (
264       <div class="mb-2">
265         {!community_view.subscribed && (
266           <a
267             class="btn btn-secondary btn-block"
268             href="#"
269             onClick={linkEvent(
270               community_view.community.id,
271               this.handleSubscribe
272             )}
273           >
274             {i18n.t('subscribe')}
275           </a>
276         )}
277       </div>
278     );
279   }
280
281   description() {
282     let description = this.props.community_view.community.description;
283     return (
284       description && (
285         <div
286           className="md-div"
287           dangerouslySetInnerHTML={mdToHtml(description)}
288         />
289       )
290     );
291   }
292
293   adminButtons() {
294     let community_view = this.props.community_view;
295     return (
296       <>
297         <ul class="list-inline mb-1 text-muted font-weight-bold">
298           {this.canMod && (
299             <>
300               <li className="list-inline-item-action">
301                 <span
302                   role="button"
303                   class="pointer"
304                   onClick={linkEvent(this, this.handleEditClick)}
305                   data-tippy-content={i18n.t('edit')}
306                   aria-label={i18n.t('edit')}
307                 >
308                   <svg class="icon icon-inline">
309                     <use xlinkHref="#icon-edit"></use>
310                   </svg>
311                 </span>
312               </li>
313               {!this.amCreator &&
314                 (!this.state.showConfirmLeaveModTeam ? (
315                   <li className="list-inline-item-action">
316                     <span
317                       class="pointer"
318                       role="button"
319                       onClick={linkEvent(
320                         this,
321                         this.handleShowConfirmLeaveModTeamClick
322                       )}
323                     >
324                       {i18n.t('leave_mod_team')}
325                     </span>
326                   </li>
327                 ) : (
328                   <>
329                     <li className="list-inline-item-action">
330                       {i18n.t('are_you_sure')}
331                     </li>
332                     <li className="list-inline-item-action">
333                       <span
334                         class="pointer"
335                         role="button"
336                         onClick={linkEvent(this, this.handleLeaveModTeamClick)}
337                       >
338                         {i18n.t('yes')}
339                       </span>
340                     </li>
341                     <li className="list-inline-item-action">
342                       <span
343                         class="pointer"
344                         role="button"
345                         onClick={linkEvent(
346                           this,
347                           this.handleCancelLeaveModTeamClick
348                         )}
349                       >
350                         {i18n.t('no')}
351                       </span>
352                     </li>
353                   </>
354                 ))}
355               {this.amCreator && (
356                 <li className="list-inline-item-action">
357                   <span
358                     class="pointer"
359                     onClick={linkEvent(this, this.handleDeleteClick)}
360                     data-tippy-content={
361                       !community_view.community.deleted
362                         ? i18n.t('delete')
363                         : i18n.t('restore')
364                     }
365                     aria-label={
366                       !community_view.community.deleted
367                         ? i18n.t('delete')
368                         : i18n.t('restore')
369                     }
370                   >
371                     <svg
372                       class={`icon icon-inline ${
373                         community_view.community.deleted && 'text-danger'
374                       }`}
375                     >
376                       <use xlinkHref="#icon-trash"></use>
377                     </svg>
378                   </span>
379                 </li>
380               )}
381             </>
382           )}
383           {this.canAdmin && (
384             <li className="list-inline-item">
385               {!this.props.community_view.community.removed ? (
386                 <span
387                   class="pointer"
388                   role="button"
389                   onClick={linkEvent(this, this.handleModRemoveShow)}
390                 >
391                   {i18n.t('remove')}
392                 </span>
393               ) : (
394                 <span
395                   class="pointer"
396                   role="button"
397                   onClick={linkEvent(this, this.handleModRemoveSubmit)}
398                 >
399                   {i18n.t('restore')}
400                 </span>
401               )}
402             </li>
403           )}
404         </ul>
405         {this.state.showRemoveDialog && (
406           <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
407             <div class="form-group row">
408               <label class="col-form-label" htmlFor="remove-reason">
409                 {i18n.t('reason')}
410               </label>
411               <input
412                 type="text"
413                 id="remove-reason"
414                 class="form-control mr-2"
415                 placeholder={i18n.t('optional')}
416                 value={this.state.removeReason}
417                 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
418               />
419             </div>
420             {/* TODO hold off on expires for now */}
421             {/* <div class="form-group row"> */}
422             {/*   <label class="col-form-label">Expires</label> */}
423             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
424             {/* </div> */}
425             <div class="form-group row">
426               <button type="submit" class="btn btn-secondary">
427                 {i18n.t('remove_community')}
428               </button>
429             </div>
430           </form>
431         )}
432       </>
433     );
434   }
435
436   handleEditClick(i: Sidebar) {
437     i.state.showEdit = true;
438     i.setState(i.state);
439   }
440
441   handleEditCommunity() {
442     this.state.showEdit = false;
443     this.setState(this.state);
444   }
445
446   handleEditCancel() {
447     this.state.showEdit = false;
448     this.setState(this.state);
449   }
450
451   handleDeleteClick(i: Sidebar, event: any) {
452     event.preventDefault();
453     let deleteForm: DeleteCommunity = {
454       community_id: i.props.community_view.community.id,
455       deleted: !i.props.community_view.community.deleted,
456       auth: authField(),
457     };
458     WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm));
459   }
460
461   handleShowConfirmLeaveModTeamClick(i: Sidebar) {
462     i.state.showConfirmLeaveModTeam = true;
463     i.setState(i.state);
464   }
465
466   handleLeaveModTeamClick(i: Sidebar) {
467     let form: AddModToCommunity = {
468       user_id: UserService.Instance.user.id,
469       community_id: i.props.community_view.community.id,
470       added: false,
471       auth: authField(),
472     };
473     WebSocketService.Instance.send(wsClient.addModToCommunity(form));
474     i.state.showConfirmLeaveModTeam = false;
475     i.setState(i.state);
476   }
477
478   handleCancelLeaveModTeamClick(i: Sidebar) {
479     i.state.showConfirmLeaveModTeam = false;
480     i.setState(i.state);
481   }
482
483   handleUnsubscribe(communityId: number, event: any) {
484     event.preventDefault();
485     let form: FollowCommunity = {
486       community_id: communityId,
487       follow: false,
488       auth: authField(),
489     };
490     WebSocketService.Instance.send(wsClient.followCommunity(form));
491   }
492
493   handleSubscribe(communityId: number, event: any) {
494     event.preventDefault();
495     let form: FollowCommunity = {
496       community_id: communityId,
497       follow: true,
498       auth: authField(),
499     };
500     WebSocketService.Instance.send(wsClient.followCommunity(form));
501   }
502
503   private get amCreator(): boolean {
504     return this.props.community_view.creator.id == UserService.Instance.user.id;
505   }
506
507   get canMod(): boolean {
508     return (
509       UserService.Instance.user &&
510       this.props.moderators
511         .map(m => m.moderator.id)
512         .includes(UserService.Instance.user.id)
513     );
514   }
515
516   get canAdmin(): boolean {
517     return (
518       UserService.Instance.user &&
519       this.props.admins
520         .map(a => a.user.id)
521         .includes(UserService.Instance.user.id)
522     );
523   }
524
525   handleModRemoveShow(i: Sidebar) {
526     i.state.showRemoveDialog = true;
527     i.setState(i.state);
528   }
529
530   handleModRemoveReasonChange(i: Sidebar, event: any) {
531     i.state.removeReason = event.target.value;
532     i.setState(i.state);
533   }
534
535   handleModRemoveExpiresChange(i: Sidebar, event: any) {
536     console.log(event.target.value);
537     i.state.removeExpires = event.target.value;
538     i.setState(i.state);
539   }
540
541   handleModRemoveSubmit(i: Sidebar, event: any) {
542     event.preventDefault();
543     let removeForm: RemoveCommunity = {
544       community_id: i.props.community_view.community.id,
545       removed: !i.props.community_view.community.removed,
546       reason: i.state.removeReason,
547       expires: getUnixTime(i.state.removeExpires),
548       auth: authField(),
549     };
550     WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
551
552     i.state.showRemoveDialog = false;
553     i.setState(i.state);
554   }
555 }