]> Untitled Git - lemmy.git/blob - ui/src/components/sidebar.tsx
Fixing online counts. Fixes #664
[lemmy.git] / ui / src / components / sidebar.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import {
4   Community,
5   CommunityUser,
6   FollowCommunityForm,
7   DeleteCommunityForm,
8   RemoveCommunityForm,
9   UserView,
10 } from '../interfaces';
11 import { WebSocketService, UserService } from '../services';
12 import { mdToHtml, getUnixTime, pictrsAvatarThumbnail } from '../utils';
13 import { CommunityForm } from './community-form';
14 import { UserListing } from './user-listing';
15 import { CommunityLink } from './community-link';
16 import { BannerIconHeader } from './banner-icon-header';
17 import { i18n } from '../i18next';
18
19 interface SidebarProps {
20   community: Community;
21   moderators: Array<CommunityUser>;
22   admins: Array<UserView>;
23   online: number;
24   enableNsfw: boolean;
25   showIcon?: boolean;
26 }
27
28 interface SidebarState {
29   showEdit: boolean;
30   showRemoveDialog: boolean;
31   removeReason: string;
32   removeExpires: string;
33 }
34
35 export class Sidebar extends Component<SidebarProps, SidebarState> {
36   private emptyState: SidebarState = {
37     showEdit: false,
38     showRemoveDialog: false,
39     removeReason: null,
40     removeExpires: null,
41   };
42
43   constructor(props: any, context: any) {
44     super(props, context);
45     this.state = this.emptyState;
46     this.handleEditCommunity = this.handleEditCommunity.bind(this);
47     this.handleEditCancel = this.handleEditCancel.bind(this);
48   }
49
50   render() {
51     return (
52       <div>
53         {!this.state.showEdit ? (
54           this.sidebar()
55         ) : (
56           <CommunityForm
57             community={this.props.community}
58             onEdit={this.handleEditCommunity}
59             onCancel={this.handleEditCancel}
60             enableNsfw={this.props.enableNsfw}
61           />
62         )}
63       </div>
64     );
65   }
66
67   sidebar() {
68     return (
69       <div>
70         <div class="card bg-transparent border-secondary mb-3">
71           <div class="card-header bg-transparent border-secondary">
72             {this.communityTitle()}
73             {this.adminButtons()}
74           </div>
75           <div class="card-body">{this.subscribes()}</div>
76         </div>
77         <div class="card bg-transparent border-secondary mb-3">
78           <div class="card-body">
79             {this.description()}
80             {this.badges()}
81             {this.mods()}
82           </div>
83         </div>
84       </div>
85     );
86   }
87
88   communityTitle() {
89     let community = this.props.community;
90     return (
91       <div>
92         <h5 className="mb-0">
93           {this.props.showIcon && (
94             <BannerIconHeader icon={community.icon} banner={community.banner} />
95           )}
96           <span>{community.title}</span>
97           {community.removed && (
98             <small className="ml-2 text-muted font-italic">
99               {i18n.t('removed')}
100             </small>
101           )}
102           {community.deleted && (
103             <small className="ml-2 text-muted font-italic">
104               {i18n.t('deleted')}
105             </small>
106           )}
107           {community.nsfw && (
108             <small className="ml-2 text-muted font-italic">
109               {i18n.t('nsfw')}
110             </small>
111           )}
112         </h5>
113         <CommunityLink
114           community={community}
115           realLink
116           useApubName
117           muted
118           hideAvatar
119         />
120       </div>
121     );
122   }
123
124   badges() {
125     let community = this.props.community;
126     return (
127       <ul class="my-1 list-inline">
128         <li className="list-inline-item badge badge-light">
129           {i18n.t('number_online', { count: this.props.online })}
130         </li>
131         <li className="list-inline-item badge badge-light">
132           {i18n.t('number_of_subscribers', {
133             count: community.number_of_subscribers,
134           })}
135         </li>
136         <li className="list-inline-item badge badge-light">
137           {i18n.t('number_of_posts', {
138             count: community.number_of_posts,
139           })}
140         </li>
141         <li className="list-inline-item badge badge-light">
142           {i18n.t('number_of_comments', {
143             count: community.number_of_comments,
144           })}
145         </li>
146         <li className="list-inline-item">
147           <Link className="badge badge-light" to="/communities">
148             {community.category_name}
149           </Link>
150         </li>
151         <li className="list-inline-item">
152           <Link
153             className="badge badge-light"
154             to={`/modlog/community/${this.props.community.id}`}
155           >
156             {i18n.t('modlog')}
157           </Link>
158         </li>
159         <li className="list-inline-item badge badge-light">
160           <CommunityLink community={community} realLink />
161         </li>
162       </ul>
163     );
164   }
165
166   mods() {
167     return (
168       <ul class="list-inline small">
169         <li class="list-inline-item">{i18n.t('mods')}: </li>
170         {this.props.moderators.map(mod => (
171           <li class="list-inline-item">
172             <UserListing
173               user={{
174                 name: mod.user_name,
175                 preferred_username: mod.user_preferred_username,
176                 avatar: mod.avatar,
177                 id: mod.user_id,
178                 local: mod.user_local,
179                 actor_id: mod.user_actor_id,
180               }}
181             />
182           </li>
183         ))}
184       </ul>
185     );
186   }
187
188   subscribes() {
189     let community = this.props.community;
190     return (
191       <div class="d-flex flex-wrap">
192         <Link
193           class={`btn btn-secondary flex-fill mr-2 mb-2 ${
194             community.deleted || community.removed ? 'no-click' : ''
195           }`}
196           to={`/create_post?community=${community.name}`}
197         >
198           {i18n.t('create_a_post')}
199         </Link>
200         {community.subscribed ? (
201           <a
202             class="btn btn-secondary flex-fill mb-2"
203             href="#"
204             onClick={linkEvent(community.id, this.handleUnsubscribe)}
205           >
206             {i18n.t('unsubscribe')}
207           </a>
208         ) : (
209           <a
210             class="btn btn-secondary flex-fill mb-2"
211             href="#"
212             onClick={linkEvent(community.id, this.handleSubscribe)}
213           >
214             {i18n.t('subscribe')}
215           </a>
216         )}
217       </div>
218     );
219   }
220
221   description() {
222     let community = this.props.community;
223     return (
224       community.description && (
225         <div
226           className="md-div"
227           dangerouslySetInnerHTML={mdToHtml(community.description)}
228         />
229       )
230     );
231   }
232
233   adminButtons() {
234     let community = this.props.community;
235     return (
236       <>
237         <ul class="list-inline mb-1 text-muted font-weight-bold">
238           {this.canMod && (
239             <>
240               <li className="list-inline-item-action">
241                 <span
242                   class="pointer"
243                   onClick={linkEvent(this, this.handleEditClick)}
244                   data-tippy-content={i18n.t('edit')}
245                 >
246                   <svg class="icon icon-inline">
247                     <use xlinkHref="#icon-edit"></use>
248                   </svg>
249                 </span>
250               </li>
251               {this.amCreator && (
252                 <li className="list-inline-item-action">
253                   <span
254                     class="pointer"
255                     onClick={linkEvent(this, this.handleDeleteClick)}
256                     data-tippy-content={
257                       !community.deleted ? i18n.t('delete') : i18n.t('restore')
258                     }
259                   >
260                     <svg
261                       class={`icon icon-inline ${
262                         community.deleted && 'text-danger'
263                       }`}
264                     >
265                       <use xlinkHref="#icon-trash"></use>
266                     </svg>
267                   </span>
268                 </li>
269               )}
270             </>
271           )}
272           {this.canAdmin && (
273             <li className="list-inline-item">
274               {!this.props.community.removed ? (
275                 <span
276                   class="pointer"
277                   onClick={linkEvent(this, this.handleModRemoveShow)}
278                 >
279                   {i18n.t('remove')}
280                 </span>
281               ) : (
282                 <span
283                   class="pointer"
284                   onClick={linkEvent(this, this.handleModRemoveSubmit)}
285                 >
286                   {i18n.t('restore')}
287                 </span>
288               )}
289             </li>
290           )}
291         </ul>
292         {this.state.showRemoveDialog && (
293           <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
294             <div class="form-group row">
295               <label class="col-form-label" htmlFor="remove-reason">
296                 {i18n.t('reason')}
297               </label>
298               <input
299                 type="text"
300                 id="remove-reason"
301                 class="form-control mr-2"
302                 placeholder={i18n.t('optional')}
303                 value={this.state.removeReason}
304                 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
305               />
306             </div>
307             {/* TODO hold off on expires for now */}
308             {/* <div class="form-group row"> */}
309             {/*   <label class="col-form-label">Expires</label> */}
310             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
311             {/* </div> */}
312             <div class="form-group row">
313               <button type="submit" class="btn btn-secondary">
314                 {i18n.t('remove_community')}
315               </button>
316             </div>
317           </form>
318         )}
319       </>
320     );
321   }
322
323   handleEditClick(i: Sidebar) {
324     i.state.showEdit = true;
325     i.setState(i.state);
326   }
327
328   handleEditCommunity() {
329     this.state.showEdit = false;
330     this.setState(this.state);
331   }
332
333   handleEditCancel() {
334     this.state.showEdit = false;
335     this.setState(this.state);
336   }
337
338   handleDeleteClick(i: Sidebar) {
339     event.preventDefault();
340     let deleteForm: DeleteCommunityForm = {
341       edit_id: i.props.community.id,
342       deleted: !i.props.community.deleted,
343     };
344     WebSocketService.Instance.deleteCommunity(deleteForm);
345   }
346
347   handleUnsubscribe(communityId: number) {
348     event.preventDefault();
349     let form: FollowCommunityForm = {
350       community_id: communityId,
351       follow: false,
352     };
353     WebSocketService.Instance.followCommunity(form);
354   }
355
356   handleSubscribe(communityId: number) {
357     event.preventDefault();
358     let form: FollowCommunityForm = {
359       community_id: communityId,
360       follow: true,
361     };
362     WebSocketService.Instance.followCommunity(form);
363   }
364
365   private get amCreator(): boolean {
366     return this.props.community.creator_id == UserService.Instance.user.id;
367   }
368
369   get canMod(): boolean {
370     return (
371       UserService.Instance.user &&
372       this.props.moderators
373         .map(m => m.user_id)
374         .includes(UserService.Instance.user.id)
375     );
376   }
377
378   get canAdmin(): boolean {
379     return (
380       UserService.Instance.user &&
381       this.props.admins.map(a => a.id).includes(UserService.Instance.user.id)
382     );
383   }
384
385   handleModRemoveShow(i: Sidebar) {
386     i.state.showRemoveDialog = true;
387     i.setState(i.state);
388   }
389
390   handleModRemoveReasonChange(i: Sidebar, event: any) {
391     i.state.removeReason = event.target.value;
392     i.setState(i.state);
393   }
394
395   handleModRemoveExpiresChange(i: Sidebar, event: any) {
396     console.log(event.target.value);
397     i.state.removeExpires = event.target.value;
398     i.setState(i.state);
399   }
400
401   handleModRemoveSubmit(i: Sidebar) {
402     event.preventDefault();
403     let removeForm: RemoveCommunityForm = {
404       edit_id: i.props.community.id,
405       removed: !i.props.community.removed,
406       reason: i.state.removeReason,
407       expires: getUnixTime(i.state.removeExpires),
408     };
409     WebSocketService.Instance.removeCommunity(removeForm);
410
411     i.state.showRemoveDialog = false;
412     i.setState(i.state);
413   }
414 }