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