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