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';
15 Overview, Comments, Posts, Saved
22 follows: Array<CommunityUser>;
23 moderates: Array<CommunityUser>;
24 comments: Array<Comment>;
27 admins: Array<UserView>;
32 userSettingsForm: UserSettingsForm;
33 userSettingsLoading: boolean;
34 deleteAccountLoading: boolean;
35 deleteAccountShowConfirm: boolean;
36 deleteAccountForm: DeleteAccountForm;
39 export class User extends Component<any, UserState> {
41 private subscription: Subscription;
42 private emptyState: UserState = {
48 number_of_posts: null,
50 number_of_comments: null,
62 view: this.getViewFromProps(this.props),
63 sort: this.getSortTypeFromProps(this.props),
64 page: this.getPageFromProps(this.props),
70 userSettingsLoading: null,
71 deleteAccountLoading: null,
72 deleteAccountShowConfirm: false,
78 constructor(props: any, context: any) {
79 super(props, context);
81 this.state = this.emptyState;
83 this.state.user_id = Number(this.props.match.params.id);
84 this.state.username = this.props.match.params.username;
86 this.subscription = WebSocketService.Instance.subject
87 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
89 (msg) => this.parseMessage(msg),
90 (err) => console.error(err),
91 () => console.log('complete')
98 return UserService.Instance.user && UserService.Instance.user.id == this.state.user.id;
101 getViewFromProps(props: any): View {
102 return (props.match.params.view) ?
103 View[capitalizeFirstLetter(props.match.params.view)] :
107 getSortTypeFromProps(props: any): SortType {
108 return (props.match.params.sort) ?
109 routeSortTypeToEnum(props.match.params.sort) :
113 getPageFromProps(props: any): number {
114 return (props.match.params.page) ? Number(props.match.params.page) : 1;
117 componentWillUnmount() {
118 this.subscription.unsubscribe();
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);
134 <div class="container">
135 {this.state.loading ?
136 <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
138 <div class="col-12 col-md-8">
139 <h5>/u/{this.state.user.name}</h5>
141 {this.state.view == View.Overview &&
144 {this.state.view == View.Comments &&
147 {this.state.view == View.Posts &&
150 {this.state.view == View.Saved &&
155 <div class="col-12 col-md-4">
157 {this.isCurrentUser &&
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>
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>
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}});
198 combined.push(...comments);
199 combined.push(...posts);
202 if (this.state.sort == SortType.New) {
203 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
205 combined.sort((a, b) => b.data.score - a.data.score);
214 post={i.data as Post}
215 admins={this.state.admins}
220 nodes={[{comment: i.data as Comment}]}
221 admins={this.state.admins}
234 {this.state.comments.map(comment =>
235 <CommentNodes nodes={[{comment: comment}]}
236 admins={this.state.admins}
246 {this.state.posts.map(post =>
249 admins={this.state.admins}
258 let user = this.state.user;
261 <div class="card border-secondary mb-3">
262 <div class="card-body">
264 <ul class="list-inline mb-0">
265 <li className="list-inline-item">{user.name}</li>
267 <li className="list-inline-item badge badge-danger"><T i18nKey="banned">#</T></li>
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">
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>
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>
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">
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>
303 <option value={theme}>{theme}</option>
308 <div class="form-group">
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>
316 <div class="form-group row mb-0">
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 &&
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>
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>
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>
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>
383 <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
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}`);
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,
417 WebSocketService.Instance.getUserDetails(form);
420 handleSortChange(i: User, event: any) {
421 i.state.sort = Number(event.target.value);
428 handleViewChange(i: User, event: any) {
429 i.state.view = Number(event.target.value);
436 handleUserSettingsShowNsfwChange(i: User, event: any) {
437 i.state.userSettingsForm.show_nsfw = event.target.checked;
441 handleUserSettingsThemeChange(i: User, event: any) {
442 i.state.userSettingsForm.theme = event.target.value;
443 setTheme(event.target.value);
447 handleUserSettingsSubmit(i: User, event: any) {
448 event.preventDefault();
449 i.state.userSettingsLoading = true;
452 WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
455 handleDeleteAccountShowConfirmToggle(i: User, event: any) {
456 event.preventDefault();
457 i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
461 handleDeleteAccountPasswordChange(i: User, event: any) {
462 i.state.deleteAccountForm.password = event.target.value;
466 handleDeleteAccount(i: User, event: any) {
467 event.preventDefault();
468 i.state.deleteAccountLoading = true;
471 WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
474 parseMessage(msg: any) {
476 let op: UserOperation = msgOp(msg);
478 alert(i18n.t(msg.error));
479 this.state.deleteAccountLoading = false;
480 this.setState(this.state);
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';
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;
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;
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('/');