]> Untitled Git - lemmy.git/blob - ui/src/components/sidebar.tsx
Merge branch 'main' into federation-authorisation
[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         {/*
129               <li className="list-inline-item badge badge-light">
130                 {i18n.t('number_online', { count: this.props.online })}
131               </li>
132               */}
133         <li className="list-inline-item badge badge-light">
134           {i18n.t('number_of_subscribers', {
135             count: community.number_of_subscribers,
136           })}
137         </li>
138         <li className="list-inline-item badge badge-light">
139           {i18n.t('number_of_posts', {
140             count: community.number_of_posts,
141           })}
142         </li>
143         <li className="list-inline-item badge badge-light">
144           {i18n.t('number_of_comments', {
145             count: community.number_of_comments,
146           })}
147         </li>
148         <li className="list-inline-item">
149           <Link className="badge badge-light" to="/communities">
150             {community.category_name}
151           </Link>
152         </li>
153         <li className="list-inline-item">
154           <Link
155             className="badge badge-light"
156             to={`/modlog/community/${this.props.community.id}`}
157           >
158             {i18n.t('modlog')}
159           </Link>
160         </li>
161         <li className="list-inline-item badge badge-light">
162           <CommunityLink community={community} realLink />
163         </li>
164       </ul>
165     );
166   }
167
168   mods() {
169     return (
170       <ul class="list-inline small">
171         <li class="list-inline-item">{i18n.t('mods')}: </li>
172         {this.props.moderators.map(mod => (
173           <li class="list-inline-item">
174             <UserListing
175               user={{
176                 name: mod.user_name,
177                 preferred_username: mod.user_preferred_username,
178                 avatar: mod.avatar,
179                 id: mod.user_id,
180                 local: mod.user_local,
181                 actor_id: mod.user_actor_id,
182               }}
183             />
184           </li>
185         ))}
186       </ul>
187     );
188   }
189
190   subscribes() {
191     let community = this.props.community;
192     return (
193       <div class="d-flex flex-wrap">
194         <Link
195           class={`btn btn-secondary flex-fill mr-2 mb-2 ${
196             community.deleted || community.removed ? 'no-click' : ''
197           }`}
198           to={`/create_post?community=${community.name}`}
199         >
200           {i18n.t('create_a_post')}
201         </Link>
202         {community.subscribed ? (
203           <a
204             class="btn btn-secondary flex-fill mb-2"
205             href="#"
206             onClick={linkEvent(community.id, this.handleUnsubscribe)}
207           >
208             {i18n.t('unsubscribe')}
209           </a>
210         ) : (
211           <a
212             class="btn btn-secondary flex-fill mb-2"
213             href="#"
214             onClick={linkEvent(community.id, this.handleSubscribe)}
215           >
216             {i18n.t('subscribe')}
217           </a>
218         )}
219       </div>
220     );
221   }
222
223   description() {
224     let community = this.props.community;
225     return (
226       community.description && (
227         <div
228           className="md-div"
229           dangerouslySetInnerHTML={mdToHtml(community.description)}
230         />
231       )
232     );
233   }
234
235   adminButtons() {
236     let community = this.props.community;
237     return (
238       <>
239         <ul class="list-inline mb-1 text-muted font-weight-bold">
240           {this.canMod && (
241             <>
242               <li className="list-inline-item-action">
243                 <span
244                   class="pointer"
245                   onClick={linkEvent(this, this.handleEditClick)}
246                   data-tippy-content={i18n.t('edit')}
247                 >
248                   <svg class="icon icon-inline">
249                     <use xlinkHref="#icon-edit"></use>
250                   </svg>
251                 </span>
252               </li>
253               {this.amCreator && (
254                 <li className="list-inline-item-action">
255                   <span
256                     class="pointer"
257                     onClick={linkEvent(this, this.handleDeleteClick)}
258                     data-tippy-content={
259                       !community.deleted ? i18n.t('delete') : i18n.t('restore')
260                     }
261                   >
262                     <svg
263                       class={`icon icon-inline ${
264                         community.deleted && 'text-danger'
265                       }`}
266                     >
267                       <use xlinkHref="#icon-trash"></use>
268                     </svg>
269                   </span>
270                 </li>
271               )}
272             </>
273           )}
274           {this.canAdmin && (
275             <li className="list-inline-item">
276               {!this.props.community.removed ? (
277                 <span
278                   class="pointer"
279                   onClick={linkEvent(this, this.handleModRemoveShow)}
280                 >
281                   {i18n.t('remove')}
282                 </span>
283               ) : (
284                 <span
285                   class="pointer"
286                   onClick={linkEvent(this, this.handleModRemoveSubmit)}
287                 >
288                   {i18n.t('restore')}
289                 </span>
290               )}
291             </li>
292           )}
293         </ul>
294         {this.state.showRemoveDialog && (
295           <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
296             <div class="form-group row">
297               <label class="col-form-label" htmlFor="remove-reason">
298                 {i18n.t('reason')}
299               </label>
300               <input
301                 type="text"
302                 id="remove-reason"
303                 class="form-control mr-2"
304                 placeholder={i18n.t('optional')}
305                 value={this.state.removeReason}
306                 onInput={linkEvent(this, this.handleModRemoveReasonChange)}
307               />
308             </div>
309             {/* TODO hold off on expires for now */}
310             {/* <div class="form-group row"> */}
311             {/*   <label class="col-form-label">Expires</label> */}
312             {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
313             {/* </div> */}
314             <div class="form-group row">
315               <button type="submit" class="btn btn-secondary">
316                 {i18n.t('remove_community')}
317               </button>
318             </div>
319           </form>
320         )}
321       </>
322     );
323   }
324
325   handleEditClick(i: Sidebar) {
326     i.state.showEdit = true;
327     i.setState(i.state);
328   }
329
330   handleEditCommunity() {
331     this.state.showEdit = false;
332     this.setState(this.state);
333   }
334
335   handleEditCancel() {
336     this.state.showEdit = false;
337     this.setState(this.state);
338   }
339
340   handleDeleteClick(i: Sidebar) {
341     event.preventDefault();
342     let deleteForm: DeleteCommunityForm = {
343       edit_id: i.props.community.id,
344       deleted: !i.props.community.deleted,
345     };
346     WebSocketService.Instance.deleteCommunity(deleteForm);
347   }
348
349   handleUnsubscribe(communityId: number) {
350     event.preventDefault();
351     let form: FollowCommunityForm = {
352       community_id: communityId,
353       follow: false,
354     };
355     WebSocketService.Instance.followCommunity(form);
356   }
357
358   handleSubscribe(communityId: number) {
359     event.preventDefault();
360     let form: FollowCommunityForm = {
361       community_id: communityId,
362       follow: true,
363     };
364     WebSocketService.Instance.followCommunity(form);
365   }
366
367   private get amCreator(): boolean {
368     return this.props.community.creator_id == UserService.Instance.user.id;
369   }
370
371   get canMod(): boolean {
372     return (
373       UserService.Instance.user &&
374       this.props.moderators
375         .map(m => m.user_id)
376         .includes(UserService.Instance.user.id)
377     );
378   }
379
380   get canAdmin(): boolean {
381     return (
382       UserService.Instance.user &&
383       this.props.admins.map(a => a.id).includes(UserService.Instance.user.id)
384     );
385   }
386
387   handleModRemoveShow(i: Sidebar) {
388     i.state.showRemoveDialog = true;
389     i.setState(i.state);
390   }
391
392   handleModRemoveReasonChange(i: Sidebar, event: any) {
393     i.state.removeReason = event.target.value;
394     i.setState(i.state);
395   }
396
397   handleModRemoveExpiresChange(i: Sidebar, event: any) {
398     console.log(event.target.value);
399     i.state.removeExpires = event.target.value;
400     i.setState(i.state);
401   }
402
403   handleModRemoveSubmit(i: Sidebar) {
404     event.preventDefault();
405     let removeForm: RemoveCommunityForm = {
406       edit_id: i.props.community.id,
407       removed: !i.props.community.removed,
408       reason: i.state.removeReason,
409       expires: getUnixTime(i.state.removeExpires),
410     };
411     WebSocketService.Instance.removeCommunity(removeForm);
412
413     i.state.showRemoveDialog = false;
414     i.setState(i.state);
415   }
416 }