]> Untitled Git - lemmy.git/blob - ui/src/components/user.tsx
Spanish translations
[lemmy.git] / ui / src / components / user.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from "rxjs";
4 import { retryWhen, delay, take } from 'rxjs/operators';
5 import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse, UserSettingsForm, LoginResponse, BanUserResponse, AddAdminResponse } from '../interfaces';
6 import { WebSocketService, UserService } from '../services';
7 import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter, themes, setTheme } from '../utils';
8 import { PostListing } from './post-listing';
9 import { CommentNodes } from './comment-nodes';
10 import { MomentTime } from './moment-time';
11 import { i18n } from '../i18next';
12 import { T } from 'inferno-i18next';
13
14 enum View {
15   Overview, Comments, Posts, Saved
16 }
17
18 interface UserState {
19   user: UserView;
20   user_id: number;
21   username: string;
22   follows: Array<CommunityUser>;
23   moderates: Array<CommunityUser>;
24   comments: Array<Comment>;
25   posts: Array<Post>;
26   saved?: Array<Post>;
27   admins: Array<UserView>;
28   view: View;
29   sort: SortType;
30   page: number;
31   loading: boolean;
32   userSettingsForm: UserSettingsForm;
33   userSettingsLoading: boolean;
34   deleteAccountLoading: boolean;
35   deleteAccountShowConfirm: boolean;
36 }
37
38 export class User extends Component<any, UserState> {
39
40   private subscription: Subscription;
41   private emptyState: UserState = {
42     user: {
43       id: null,
44       name: null,
45       fedi_name: null,
46       published: null,
47       number_of_posts: null,
48       post_score: null,
49       number_of_comments: null,
50       comment_score: null,
51       banned: null,
52     },
53     user_id: null,
54     username: null,
55     follows: [],
56     moderates: [],
57     comments: [],
58     posts: [],
59     admins: [],
60     loading: true,
61     view: this.getViewFromProps(this.props),
62     sort: this.getSortTypeFromProps(this.props),
63     page: this.getPageFromProps(this.props),
64     userSettingsForm: {
65       show_nsfw: null,
66       theme: null,
67       auth: null,
68     },
69     userSettingsLoading: null,
70     deleteAccountLoading: null,
71     deleteAccountShowConfirm: false,
72   }
73
74   constructor(props: any, context: any) {
75     super(props, context);
76
77     this.state = this.emptyState;
78
79     this.state.user_id = Number(this.props.match.params.id);
80     this.state.username = this.props.match.params.username;
81
82     this.subscription = WebSocketService.Instance.subject
83     .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
84     .subscribe(
85       (msg) => this.parseMessage(msg),
86         (err) => console.error(err),
87         () => console.log('complete')
88     );
89
90     this.refetch();
91   }
92
93   get isCurrentUser() {
94     return UserService.Instance.user && UserService.Instance.user.id == this.state.user.id;
95   }
96
97   getViewFromProps(props: any): View {
98     return (props.match.params.view) ? 
99       View[capitalizeFirstLetter(props.match.params.view)] : 
100       View.Overview;
101   }
102
103   getSortTypeFromProps(props: any): SortType {
104     return (props.match.params.sort) ? 
105       routeSortTypeToEnum(props.match.params.sort) : 
106       SortType.New;
107   }
108
109   getPageFromProps(props: any): number {
110     return (props.match.params.page) ? Number(props.match.params.page) : 1;
111   }
112
113   componentWillUnmount() {
114     this.subscription.unsubscribe();
115   }
116
117   // Necessary for back button for some reason
118   componentWillReceiveProps(nextProps: any) {
119     if (nextProps.history.action == 'POP') {
120       this.state = this.emptyState;
121       this.state.view = this.getViewFromProps(nextProps);
122       this.state.sort = this.getSortTypeFromProps(nextProps);
123       this.state.page = this.getPageFromProps(nextProps);
124       this.refetch();
125     }
126   }
127
128   render() {
129     return (
130       <div class="container">
131         {this.state.loading ? 
132         <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : 
133         <div class="row">
134           <div class="col-12 col-md-8">
135             <h5>/u/{this.state.user.name}</h5>
136             {this.selects()}
137             {this.state.view == View.Overview &&
138               this.overview()
139             }
140             {this.state.view == View.Comments &&
141               this.comments()
142             }
143             {this.state.view == View.Posts &&
144               this.posts()
145             }
146             {this.state.view == View.Saved &&
147               this.overview()
148             }
149             {this.paginator()}
150           </div>
151           <div class="col-12 col-md-4">
152             {this.userInfo()}
153             {this.isCurrentUser &&
154               this.userSettings()
155             }
156             {this.moderates()}
157             {this.follows()}
158           </div>
159         </div>
160         }
161       </div>
162     )
163   }
164
165   selects() {
166     return (
167       <div className="mb-2">
168         <select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select custom-select-sm w-auto">
169           <option disabled><T i18nKey="view">#</T></option>
170           <option value={View.Overview}><T i18nKey="overview">#</T></option>
171           <option value={View.Comments}><T i18nKey="comments">#</T></option>
172           <option value={View.Posts}><T i18nKey="posts">#</T></option>
173           <option value={View.Saved}><T i18nKey="saved">#</T></option>
174         </select>
175         <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
176           <option disabled><T i18nKey="sort_type">#</T></option>
177           <option value={SortType.New}><T i18nKey="new">#</T></option>
178           <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
179           <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
180           <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
181           <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
182           <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
183         </select>
184       </div>
185     )
186
187   }
188
189   overview() {
190     let combined: Array<{type_: string, data: Comment | Post}> = [];
191     let comments = this.state.comments.map(e => {return {type_: "comments", data: e}});
192     let posts = this.state.posts.map(e => {return {type_: "posts", data: e}});
193
194     combined.push(...comments);
195     combined.push(...posts);
196
197     // Sort it
198     if (this.state.sort == SortType.New) {
199       combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
200     } else {
201       combined.sort((a, b) => b.data.score - a.data.score);
202     }
203
204     return (
205       <div>
206         {combined.map(i =>
207           <div>
208             {i.type_ == "posts"
209               ? <PostListing 
210               post={i.data as Post} 
211               admins={this.state.admins}
212               showCommunity 
213               viewOnly />
214               : 
215               <CommentNodes 
216                 nodes={[{comment: i.data as Comment}]} 
217                 admins={this.state.admins}
218                 noIndent />
219             }
220           </div>
221                      )
222         }
223       </div>
224     )
225   }
226
227   comments() {
228     return (
229       <div>
230         {this.state.comments.map(comment => 
231           <CommentNodes nodes={[{comment: comment}]} 
232             admins={this.state.admins}
233             noIndent />
234         )}
235       </div>
236     );
237   }
238
239   posts() {
240     return (
241       <div>
242         {this.state.posts.map(post => 
243           <PostListing 
244             post={post} 
245             admins={this.state.admins}
246             showCommunity 
247             viewOnly />
248         )}
249       </div>
250     );
251   }
252
253   userInfo() {
254     let user = this.state.user;
255     return (
256       <div>
257         <div class="card border-secondary mb-3">
258           <div class="card-body">
259             <h5>
260               <ul class="list-inline mb-0">
261                 <li className="list-inline-item">{user.name}</li>
262                 {user.banned &&  
263                   <li className="list-inline-item badge badge-danger"><T i18nKey="banned">#</T></li>
264                 }
265               </ul>
266             </h5>
267             <div>{i18n.t('joined')} <MomentTime data={user} /></div>
268             <div class="table-responsive">
269               <table class="table table-bordered table-sm mt-2 mb-0">
270                 <tr>
271                   <td><T i18nKey="number_of_points" interpolation={{count: user.post_score}}>#</T></td>
272                   <td><T i18nKey="number_of_posts" interpolation={{count: user.number_of_posts}}>#</T></td>
273                 </tr>
274                 <tr>
275                   <td><T i18nKey="number_of_points" interpolation={{count: user.comment_score}}>#</T></td>
276                   <td><T i18nKey="number_of_comments" interpolation={{count: user.number_of_comments}}>#</T></td>
277                 </tr>
278               </table>
279             </div>
280           </div>
281         </div>
282       </div>
283     )
284   }
285
286   userSettings() {  
287     return (
288       <div>
289         <div class="card border-secondary mb-3">
290           <div class="card-body">
291             <h5><T i18nKey="settings">#</T></h5>
292             <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
293               <div class="form-group">
294                 <div class="col-12">
295                   <label><T i18nKey="theme">#</T></label>
296                   <select value={this.state.userSettingsForm.theme} onChange={linkEvent(this, this.handleUserSettingsThemeChange)} class="ml-2 custom-select custom-select-sm w-auto">
297                     <option disabled><T i18nKey="theme">#</T></option>
298                     {themes.map(theme =>
299                       <option value={theme}>{theme}</option>
300                     )}
301                   </select>
302                 </div>
303               </div>
304               <div class="form-group">
305                 <div class="col-12">
306                   <div class="form-check">
307                     <input class="form-check-input" type="checkbox" checked={this.state.userSettingsForm.show_nsfw} onChange={linkEvent(this, this.handleUserSettingsShowNsfwChange)}/>
308                     <label class="form-check-label"><T i18nKey="show_nsfw">#</T></label>
309                   </div>
310                 </div>
311               </div>
312               <div class="form-group row mb-0">
313                 <div class="col-12">
314                   <button type="submit" class="btn btn-secondary mr-4">{this.state.userSettingsLoading ? 
315                   <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : capitalizeFirstLetter(i18n.t('save'))}</button>
316                   <button class="btn btn-danger" onClick={linkEvent(this, this.handleDeleteAccountShowConfirmToggle)}><T i18nKey="delete_account">#</T></button>
317                   {this.state.deleteAccountShowConfirm && 
318                     <>
319                       <div class="mt-2 alert alert-danger" role="alert"><T i18nKey="delete_account_confirm">#</T></div>
320                       <button class="btn btn-danger mr-4" onClick={linkEvent(this, this.handleDeleteAccount)}>{this.state.deleteAccountLoading ? 
321                       <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : capitalizeFirstLetter(i18n.t('yes'))}</button>
322                       <button class="btn btn-secondary" onClick={linkEvent(this, this.handleDeleteAccountShowConfirmToggle)}><T i18nKey="cancel">#</T></button>
323                     </>
324                   }
325                 </div>
326               </div>
327             </form>
328           </div>
329         </div>
330       </div>
331     )
332   }
333
334   moderates() {
335     return (
336       <div>
337         {this.state.moderates.length > 0 &&
338           <div class="card border-secondary mb-3">
339             <div class="card-body">
340               <h5><T i18nKey="moderates">#</T></h5>
341               <ul class="list-unstyled mb-0"> 
342                 {this.state.moderates.map(community =>
343                   <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
344                 )}
345               </ul>
346             </div>
347           </div>
348         }
349       </div>
350     )
351   }
352
353   follows() {
354     return (
355       <div>
356         {this.state.follows.length > 0 &&
357           <div class="card border-secondary mb-3">
358             <div class="card-body">
359               <h5><T i18nKey="subscribed">#</T></h5>
360               <ul class="list-unstyled mb-0"> 
361                 {this.state.follows.map(community =>
362                   <li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li>
363                 )}
364               </ul>
365             </div>
366           </div>
367         }
368       </div>
369     )
370   }
371
372   paginator() {
373     return (
374       <div class="my-2">
375         {this.state.page > 1 && 
376           <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
377         }
378         <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
379       </div>
380     );
381   }
382
383   updateUrl() {
384     let viewStr = View[this.state.view].toLowerCase();
385     let sortStr = SortType[this.state.sort].toLowerCase();
386     this.props.history.push(`/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`);
387   }
388
389   nextPage(i: User) { 
390     i.state.page++;
391     i.setState(i.state);
392     i.updateUrl();
393     i.refetch();
394   }
395
396   prevPage(i: User) { 
397     i.state.page--;
398     i.setState(i.state);
399     i.updateUrl();
400     i.refetch();
401   }
402
403   refetch() {
404     let form: GetUserDetailsForm = {
405       user_id: this.state.user_id,
406       username: this.state.username,
407       sort: SortType[this.state.sort],
408       saved_only: this.state.view == View.Saved,
409       page: this.state.page,
410       limit: fetchLimit,
411     };
412     WebSocketService.Instance.getUserDetails(form);
413   }
414
415   handleSortChange(i: User, event: any) {
416     i.state.sort = Number(event.target.value);
417     i.state.page = 1;
418     i.setState(i.state);
419     i.updateUrl();
420     i.refetch();
421   }
422
423   handleViewChange(i: User, event: any) {
424     i.state.view = Number(event.target.value);
425     i.state.page = 1;
426     i.setState(i.state);
427     i.updateUrl();
428     i.refetch();
429   }
430
431   handleUserSettingsShowNsfwChange(i: User, event: any) {
432     i.state.userSettingsForm.show_nsfw = event.target.checked;
433     i.setState(i.state);
434   }
435
436   handleUserSettingsThemeChange(i: User, event: any) {
437     i.state.userSettingsForm.theme = event.target.value;
438     setTheme(event.target.value);
439     i.setState(i.state);
440   }
441
442   handleUserSettingsSubmit(i: User, event: any) {
443     event.preventDefault();
444     i.state.userSettingsLoading = true;
445     i.setState(i.state);
446
447     WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
448   }
449
450   handleDeleteAccountShowConfirmToggle(i: User, event: any) {
451     event.preventDefault();
452     i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
453     i.setState(i.state);
454   }
455
456   handleDeleteAccount(i: User, event: any) {
457     event.preventDefault();
458     i.state.deleteAccountLoading = true;
459     i.setState(i.state);
460
461     WebSocketService.Instance.deleteAccount();
462   }
463
464   parseMessage(msg: any) {
465     console.log(msg);
466     let op: UserOperation = msgOp(msg);
467     if (msg.error) {
468       alert(i18n.t(msg.error));
469       return;
470     } else if (op == UserOperation.GetUserDetails) {
471       let res: UserDetailsResponse = msg;
472       this.state.user = res.user;
473       this.state.comments = res.comments;
474       this.state.follows = res.follows;
475       this.state.moderates = res.moderates;
476       this.state.posts = res.posts;
477       this.state.admins = res.admins;
478       this.state.loading = false;
479       if (this.isCurrentUser) {
480         this.state.userSettingsForm.show_nsfw = UserService.Instance.user.show_nsfw;
481         this.state.userSettingsForm.theme = UserService.Instance.user.theme ? UserService.Instance.user.theme : 'darkly';
482       }
483       document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
484       window.scrollTo(0,0);
485       this.setState(this.state);
486     } else if (op == UserOperation.EditComment) {
487       let res: CommentResponse = msg;
488
489       let found = this.state.comments.find(c => c.id == res.comment.id);
490       found.content = res.comment.content;
491       found.updated = res.comment.updated;
492       found.removed = res.comment.removed;
493       found.deleted = res.comment.deleted;
494       found.upvotes = res.comment.upvotes;
495       found.downvotes = res.comment.downvotes;
496       found.score = res.comment.score;
497
498       this.setState(this.state);
499     } else if (op == UserOperation.CreateComment) {
500       // let res: CommentResponse = msg;
501       alert(i18n.t('reply_sent'));
502       // this.state.comments.unshift(res.comment); // TODO do this right
503       // this.setState(this.state);
504     } else if (op == UserOperation.SaveComment) {
505       let res: CommentResponse = msg;
506       let found = this.state.comments.find(c => c.id == res.comment.id);
507       found.saved = res.comment.saved;
508       this.setState(this.state);
509     } else if (op == UserOperation.CreateCommentLike) {
510       let res: CommentResponse = msg;
511       let found: Comment = this.state.comments.find(c => c.id === res.comment.id);
512       found.score = res.comment.score;
513       found.upvotes = res.comment.upvotes;
514       found.downvotes = res.comment.downvotes;
515       if (res.comment.my_vote !== null) 
516         found.my_vote = res.comment.my_vote;
517       this.setState(this.state);
518     } else if (op == UserOperation.BanUser) {
519       let res: BanUserResponse = msg;
520       this.state.comments.filter(c => c.creator_id == res.user.id)
521       .forEach(c => c.banned = res.banned);
522       this.state.posts.filter(c => c.creator_id == res.user.id)
523       .forEach(c => c.banned = res.banned);
524       this.setState(this.state);
525     } else if (op == UserOperation.AddAdmin) {
526       let res: AddAdminResponse = msg;
527       this.state.admins = res.admins;
528       this.setState(this.state);
529     } else if (op == UserOperation.SaveUserSettings) {
530         this.state = this.emptyState;
531         this.state.userSettingsLoading = false;
532         this.setState(this.state);
533         let res: LoginResponse = msg;
534         UserService.Instance.login(res);
535     } else if (op == UserOperation.DeleteAccount) {
536         this.state.deleteAccountLoading = false;
537         this.state.deleteAccountShowConfirm = false;
538         this.setState(this.state);
539         this.context.router.history.push('/');
540     }
541   }
542 }
543