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';
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;
38 export class User extends Component<any, UserState> {
40 private subscription: Subscription;
41 private emptyState: UserState = {
47 number_of_posts: null,
49 number_of_comments: null,
61 view: this.getViewFromProps(this.props),
62 sort: this.getSortTypeFromProps(this.props),
63 page: this.getPageFromProps(this.props),
69 userSettingsLoading: null,
70 deleteAccountLoading: null,
71 deleteAccountShowConfirm: false,
74 constructor(props: any, context: any) {
75 super(props, context);
77 this.state = this.emptyState;
79 this.state.user_id = Number(this.props.match.params.id);
80 this.state.username = this.props.match.params.username;
82 this.subscription = WebSocketService.Instance.subject
83 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
85 (msg) => this.parseMessage(msg),
86 (err) => console.error(err),
87 () => console.log('complete')
94 return UserService.Instance.user && UserService.Instance.user.id == this.state.user.id;
97 getViewFromProps(props: any): View {
98 return (props.match.params.view) ?
99 View[capitalizeFirstLetter(props.match.params.view)] :
103 getSortTypeFromProps(props: any): SortType {
104 return (props.match.params.sort) ?
105 routeSortTypeToEnum(props.match.params.sort) :
109 getPageFromProps(props: any): number {
110 return (props.match.params.page) ? Number(props.match.params.page) : 1;
113 componentWillUnmount() {
114 this.subscription.unsubscribe();
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);
130 <div class="container">
131 {this.state.loading ?
132 <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
134 <div class="col-12 col-md-8">
135 <h5>/u/{this.state.user.name}</h5>
137 {this.state.view == View.Overview &&
140 {this.state.view == View.Comments &&
143 {this.state.view == View.Posts &&
146 {this.state.view == View.Saved &&
151 <div class="col-12 col-md-4">
153 {this.isCurrentUser &&
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>
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>
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}});
194 combined.push(...comments);
195 combined.push(...posts);
198 if (this.state.sort == SortType.New) {
199 combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
201 combined.sort((a, b) => b.data.score - a.data.score);
210 post={i.data as Post}
211 admins={this.state.admins}
216 nodes={[{comment: i.data as Comment}]}
217 admins={this.state.admins}
230 {this.state.comments.map(comment =>
231 <CommentNodes nodes={[{comment: comment}]}
232 admins={this.state.admins}
242 {this.state.posts.map(post =>
245 admins={this.state.admins}
254 let user = this.state.user;
257 <div class="card border-secondary mb-3">
258 <div class="card-body">
260 <ul class="list-inline mb-0">
261 <li className="list-inline-item">{user.name}</li>
263 <li className="list-inline-item badge badge-danger"><T i18nKey="banned">#</T></li>
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">
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>
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>
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">
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>
299 <option value={theme}>{theme}</option>
304 <div class="form-group">
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>
312 <div class="form-group row mb-0">
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 &&
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>
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>
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>
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>
378 <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
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}`);
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,
412 WebSocketService.Instance.getUserDetails(form);
415 handleSortChange(i: User, event: any) {
416 i.state.sort = Number(event.target.value);
423 handleViewChange(i: User, event: any) {
424 i.state.view = Number(event.target.value);
431 handleUserSettingsShowNsfwChange(i: User, event: any) {
432 i.state.userSettingsForm.show_nsfw = event.target.checked;
436 handleUserSettingsThemeChange(i: User, event: any) {
437 i.state.userSettingsForm.theme = event.target.value;
438 setTheme(event.target.value);
442 handleUserSettingsSubmit(i: User, event: any) {
443 event.preventDefault();
444 i.state.userSettingsLoading = true;
447 WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
450 handleDeleteAccountShowConfirmToggle(i: User, event: any) {
451 event.preventDefault();
452 i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm;
456 handleDeleteAccount(i: User, event: any) {
457 event.preventDefault();
458 i.state.deleteAccountLoading = true;
461 WebSocketService.Instance.deleteAccount();
464 parseMessage(msg: any) {
466 let op: UserOperation = msgOp(msg);
468 alert(i18n.t(msg.error));
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';
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;
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;
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('/');