]> Untitled Git - lemmy-ui.git/blob - src/shared/components/sidebar.tsx
Adding tippy to active users badges. Fixes #150
[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                   class="pointer"
303                   onClick={linkEvent(this, this.handleEditClick)}
304                   data-tippy-content={i18n.t('edit')}
305                 >
306                   <svg class="icon icon-inline">
307                     <use xlinkHref="#icon-edit"></use>
308                   </svg>
309                 </span>
310               </li>
311               {!this.amCreator &&
312                 (!this.state.showConfirmLeaveModTeam ? (
313                   <li className="list-inline-item-action">
314                     <span
315                       class="pointer"
316                       onClick={linkEvent(
317                         this,
318                         this.handleShowConfirmLeaveModTeamClick
319                       )}
320                     >
321                       {i18n.t('leave_mod_team')}
322                     </span>
323                   </li>
324                 ) : (
325                   <>
326                     <li className="list-inline-item-action">
327                       {i18n.t('are_you_sure')}
328                     </li>
329                     <li className="list-inline-item-action">
330                       <span
331                         class="pointer"
332                         onClick={linkEvent(this, this.handleLeaveModTeamClick)}
333                       >
334                         {i18n.t('yes')}
335                       </span>
336                     </li>
337                     <li className="list-inline-item-action">
338                       <span
339                         class="pointer"
340                         onClick={linkEvent(
341                           this,
342                           this.handleCancelLeaveModTeamClick
343                         )}
344                       >
345                         {i18n.t('no')}
346                       </span>
347                     </li>
348                   </>
349                 ))}
350               {this.amCreator && (
351                 <li className="list-inline-item-action">
352                   <span
353                     class="pointer"
354                     onClick={linkEvent(this, this.handleDeleteClick)}
355                     data-tippy-content={
356                       !community_view.community.deleted
357                         ? i18n.t('delete')
358                         : i18n.t('restore')
359                     }
360                   >
361                     <svg
362                       class={`icon icon-inline ${
363                         community_view.community.deleted && 'text-danger'
364                       }`}
365                     >
366                       <use xlinkHref="#icon-trash"></use>
367                     </svg>
368                   </span>
369                 </li>
370               )}
371             </>
372           )}
373           {this.canAdmin && (
374             <li className="list-inline-item">
375               {!this.props.community_view.community.removed ? (
376                 <span
377                   class="pointer"
378                   onClick={linkEvent(this, this.handleModRemoveShow)}
379                 >
380                   {i18n.t('remove')}
381                 </span>
382               ) : (
383                 <span
384                   class="pointer"
385                   onClick={linkEvent(this, this.handleModRemoveSubmit)}
386                 >
387                   {i18n.t('restore')}
388                 </span>
389               )}
390             </li>
391           )}
392         </ul>
393         {this.state.showRemoveDialog && (
394           <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
395             <div class="form-group row">
396               <label class="col-form-label" htmlFor="remove-reason">
397                 {i18n.t('reason')}
398               </label>
399               <input
400                 type="text"
401                 id="remove-reason"
402                 class="form-control mr-2"
403                 placeholder={i18n.t('optional')}
404                 value={this.state.removeReason}
405                 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
406               />
407             </div>
408             {/* TODO hold off on expires for now */}
409             {/* <div class="form-group row"> */}
410             {/*   <label class="col-form-label">Expires</label> */}
411             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
412             {/* </div> */}
413             <div class="form-group row">
414               <button type="submit" class="btn btn-secondary">
415                 {i18n.t('remove_community')}
416               </button>
417             </div>
418           </form>
419         )}
420       </>
421     );
422   }
423
424   handleEditClick(i: Sidebar) {
425     i.state.showEdit = true;
426     i.setState(i.state);
427   }
428
429   handleEditCommunity() {
430     this.state.showEdit = false;
431     this.setState(this.state);
432   }
433
434   handleEditCancel() {
435     this.state.showEdit = false;
436     this.setState(this.state);
437   }
438
439   handleDeleteClick(i: Sidebar, event: any) {
440     event.preventDefault();
441     let deleteForm: DeleteCommunity = {
442       community_id: i.props.community_view.community.id,
443       deleted: !i.props.community_view.community.deleted,
444       auth: authField(),
445     };
446     WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm));
447   }
448
449   handleShowConfirmLeaveModTeamClick(i: Sidebar) {
450     i.state.showConfirmLeaveModTeam = true;
451     i.setState(i.state);
452   }
453
454   handleLeaveModTeamClick(i: Sidebar) {
455     let form: AddModToCommunity = {
456       user_id: UserService.Instance.user.id,
457       community_id: i.props.community_view.community.id,
458       added: false,
459       auth: authField(),
460     };
461     WebSocketService.Instance.send(wsClient.addModToCommunity(form));
462     i.state.showConfirmLeaveModTeam = false;
463     i.setState(i.state);
464   }
465
466   handleCancelLeaveModTeamClick(i: Sidebar) {
467     i.state.showConfirmLeaveModTeam = false;
468     i.setState(i.state);
469   }
470
471   handleUnsubscribe(communityId: number, event: any) {
472     event.preventDefault();
473     let form: FollowCommunity = {
474       community_id: communityId,
475       follow: false,
476       auth: authField(),
477     };
478     WebSocketService.Instance.send(wsClient.followCommunity(form));
479   }
480
481   handleSubscribe(communityId: number, event: any) {
482     event.preventDefault();
483     let form: FollowCommunity = {
484       community_id: communityId,
485       follow: true,
486       auth: authField(),
487     };
488     WebSocketService.Instance.send(wsClient.followCommunity(form));
489   }
490
491   private get amCreator(): boolean {
492     return this.props.community_view.creator.id == UserService.Instance.user.id;
493   }
494
495   get canMod(): boolean {
496     return (
497       UserService.Instance.user &&
498       this.props.moderators
499         .map(m => m.moderator.id)
500         .includes(UserService.Instance.user.id)
501     );
502   }
503
504   get canAdmin(): boolean {
505     return (
506       UserService.Instance.user &&
507       this.props.admins
508         .map(a => a.user.id)
509         .includes(UserService.Instance.user.id)
510     );
511   }
512
513   handleModRemoveShow(i: Sidebar) {
514     i.state.showRemoveDialog = true;
515     i.setState(i.state);
516   }
517
518   handleModRemoveReasonChange(i: Sidebar, event: any) {
519     i.state.removeReason = event.target.value;
520     i.setState(i.state);
521   }
522
523   handleModRemoveExpiresChange(i: Sidebar, event: any) {
524     console.log(event.target.value);
525     i.state.removeExpires = event.target.value;
526     i.setState(i.state);
527   }
528
529   handleModRemoveSubmit(i: Sidebar, event: any) {
530     event.preventDefault();
531     let removeForm: RemoveCommunity = {
532       community_id: i.props.community_view.community.id,
533       removed: !i.props.community_view.community.removed,
534       reason: i.state.removeReason,
535       expires: getUnixTime(i.state.removeExpires),
536       auth: authField(),
537     };
538     WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
539
540     i.state.showRemoveDialog = false;
541     i.setState(i.state);
542   }
543 }