]> Untitled Git - lemmy.git/blob - ui/src/components/post.tsx
Merge branch 'dev' into federation
[lemmy.git] / ui / src / components / post.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Subscription } from 'rxjs';
3 import { retryWhen, delay, take } from 'rxjs/operators';
4 import {
5   UserOperation,
6   Community,
7   Post as PostI,
8   GetPostResponse,
9   PostResponse,
10   Comment,
11   CommentForm as CommentFormI,
12   CommentResponse,
13   CommentSortType,
14   CommunityUser,
15   CommunityResponse,
16   CommentNode as CommentNodeI,
17   BanFromCommunityResponse,
18   BanUserResponse,
19   AddModToCommunityResponse,
20   AddAdminResponse,
21   UserView,
22   SearchType,
23   SortType,
24   SearchForm,
25   GetPostForm,
26   SearchResponse,
27   GetSiteResponse,
28   GetCommunityResponse,
29   WebSocketJsonResponse,
30 } from '../interfaces';
31 import { WebSocketService, UserService } from '../services';
32 import { wsJsonToRes, hotRank, toast } from '../utils';
33 import { PostListing } from './post-listing';
34 import { PostListings } from './post-listings';
35 import { Sidebar } from './sidebar';
36 import { CommentForm } from './comment-form';
37 import { CommentNodes } from './comment-nodes';
38 import autosize from 'autosize';
39 import { i18n } from '../i18next';
40
41 interface PostState {
42   post: PostI;
43   comments: Array<Comment>;
44   commentSort: CommentSortType;
45   community: Community;
46   moderators: Array<CommunityUser>;
47   admins: Array<UserView>;
48   online: number;
49   scrolled?: boolean;
50   scrolled_comment_id?: number;
51   loading: boolean;
52   crossPosts: Array<PostI>;
53 }
54
55 export class Post extends Component<any, PostState> {
56   private subscription: Subscription;
57   private emptyState: PostState = {
58     post: null,
59     comments: [],
60     commentSort: CommentSortType.Hot,
61     community: null,
62     moderators: [],
63     admins: [],
64     online: null,
65     scrolled: false,
66     loading: true,
67     crossPosts: [],
68   };
69
70   constructor(props: any, context: any) {
71     super(props, context);
72
73     this.state = this.emptyState;
74
75     let postId = Number(this.props.match.params.id);
76     if (this.props.match.params.comment_id) {
77       this.state.scrolled_comment_id = this.props.match.params.comment_id;
78     }
79
80     this.subscription = WebSocketService.Instance.subject
81       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
82       .subscribe(
83         msg => this.parseMessage(msg),
84         err => console.error(err),
85         () => console.log('complete')
86       );
87
88     let form: GetPostForm = {
89       id: postId,
90     };
91     WebSocketService.Instance.getPost(form);
92   }
93
94   componentWillUnmount() {
95     this.subscription.unsubscribe();
96   }
97
98   componentDidMount() {
99     autosize(document.querySelectorAll('textarea'));
100   }
101
102   componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
103     if (
104       this.state.scrolled_comment_id &&
105       !this.state.scrolled &&
106       lastState.comments.length > 0
107     ) {
108       var elmnt = document.getElementById(
109         `comment-${this.state.scrolled_comment_id}`
110       );
111       elmnt.scrollIntoView();
112       elmnt.classList.add('mark');
113       this.state.scrolled = true;
114       this.markScrolledAsRead(this.state.scrolled_comment_id);
115     }
116
117     // Necessary if you are on a post and you click another post (same route)
118     if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
119       // Couldnt get a refresh working. This does for now.
120       location.reload();
121
122       // let currentId = this.props.match.params.id;
123       // WebSocketService.Instance.getPost(currentId);
124       // this.context.router.history.push('/sponsors');
125       // this.context.refresh();
126       // this.context.router.history.push(_lastProps.location.pathname);
127     }
128   }
129
130   markScrolledAsRead(commentId: number) {
131     let found = this.state.comments.find(c => c.id == commentId);
132     let parent = this.state.comments.find(c => found.parent_id == c.id);
133     let parent_user_id = parent
134       ? parent.creator_id
135       : this.state.post.creator_id;
136
137     if (
138       UserService.Instance.user &&
139       UserService.Instance.user.id == parent_user_id
140     ) {
141       let form: CommentFormI = {
142         content: found.content,
143         edit_id: found.id,
144         creator_id: found.creator_id,
145         post_id: found.post_id,
146         parent_id: found.parent_id,
147         read: true,
148         auth: null,
149       };
150       WebSocketService.Instance.editComment(form);
151     }
152   }
153
154   render() {
155     return (
156       <div class="container">
157         {this.state.loading ? (
158           <h5>
159             <svg class="icon icon-spinner spin">
160               <use xlinkHref="#icon-spinner"></use>
161             </svg>
162           </h5>
163         ) : (
164           <div class="row">
165             <div class="col-12 col-md-8 mb-3">
166               <PostListing
167                 post={this.state.post}
168                 showBody
169                 showCommunity
170                 moderators={this.state.moderators}
171                 admins={this.state.admins}
172               />
173               {this.state.crossPosts.length > 0 && (
174                 <>
175                   <div class="my-1 text-muted small font-weight-bold">
176                     {i18n.t('cross_posts')}
177                   </div>
178                   <PostListings showCommunity posts={this.state.crossPosts} />
179                 </>
180               )}
181               <div className="mb-2" />
182               <CommentForm
183                 postId={this.state.post.id}
184                 disabled={this.state.post.locked}
185               />
186               {this.state.comments.length > 0 && this.sortRadios()}
187               {this.commentsTree()}
188             </div>
189             <div class="col-12 col-sm-12 col-md-4">
190               {this.state.comments.length > 0 && this.newComments()}
191               {this.sidebar()}
192             </div>
193           </div>
194         )}
195       </div>
196     );
197   }
198
199   sortRadios() {
200     return (
201       <div class="btn-group btn-group-toggle mb-3">
202         <label
203           className={`btn btn-sm btn-secondary pointer ${this.state
204             .commentSort === CommentSortType.Hot && 'active'}`}
205         >
206           {i18n.t('hot')}
207           <input
208             type="radio"
209             value={CommentSortType.Hot}
210             checked={this.state.commentSort === CommentSortType.Hot}
211             onChange={linkEvent(this, this.handleCommentSortChange)}
212           />
213         </label>
214         <label
215           className={`btn btn-sm btn-secondary pointer ${this.state
216             .commentSort === CommentSortType.Top && 'active'}`}
217         >
218           {i18n.t('top')}
219           <input
220             type="radio"
221             value={CommentSortType.Top}
222             checked={this.state.commentSort === CommentSortType.Top}
223             onChange={linkEvent(this, this.handleCommentSortChange)}
224           />
225         </label>
226         <label
227           className={`btn btn-sm btn-secondary pointer ${this.state
228             .commentSort === CommentSortType.New && 'active'}`}
229         >
230           {i18n.t('new')}
231           <input
232             type="radio"
233             value={CommentSortType.New}
234             checked={this.state.commentSort === CommentSortType.New}
235             onChange={linkEvent(this, this.handleCommentSortChange)}
236           />
237         </label>
238         <label
239           className={`btn btn-sm btn-secondary pointer ${this.state
240             .commentSort === CommentSortType.Old && 'active'}`}
241         >
242           {i18n.t('old')}
243           <input
244             type="radio"
245             value={CommentSortType.Old}
246             checked={this.state.commentSort === CommentSortType.Old}
247             onChange={linkEvent(this, this.handleCommentSortChange)}
248           />
249         </label>
250       </div>
251     );
252   }
253
254   newComments() {
255     return (
256       <div class="d-none d-md-block new-comments mb-3 card border-secondary">
257         <div class="card-body small">
258           <h6>{i18n.t('recent_comments')}</h6>
259           {this.state.comments.map(comment => (
260             <CommentNodes
261               nodes={[{ comment: comment }]}
262               noIndent
263               locked={this.state.post.locked}
264               moderators={this.state.moderators}
265               admins={this.state.admins}
266               postCreatorId={this.state.post.creator_id}
267             />
268           ))}
269         </div>
270       </div>
271     );
272   }
273
274   sidebar() {
275     return (
276       <div class="mb-3">
277         <Sidebar
278           community={this.state.community}
279           moderators={this.state.moderators}
280           admins={this.state.admins}
281           online={this.state.online}
282         />
283       </div>
284     );
285   }
286
287   handleCommentSortChange(i: Post, event: any) {
288     i.state.commentSort = Number(event.target.value);
289     i.setState(i.state);
290   }
291
292   private buildCommentsTree(): Array<CommentNodeI> {
293     let map = new Map<number, CommentNodeI>();
294     for (let comment of this.state.comments) {
295       let node: CommentNodeI = {
296         comment: comment,
297         children: [],
298       };
299       map.set(comment.id, { ...node });
300     }
301     let tree: Array<CommentNodeI> = [];
302     for (let comment of this.state.comments) {
303       if (comment.parent_id) {
304         map.get(comment.parent_id).children.push(map.get(comment.id));
305       } else {
306         tree.push(map.get(comment.id));
307       }
308     }
309
310     this.sortTree(tree);
311
312     return tree;
313   }
314
315   sortTree(tree: Array<CommentNodeI>) {
316     // First, put removed and deleted comments at the bottom, then do your other sorts
317     if (this.state.commentSort == CommentSortType.Top) {
318       tree.sort(
319         (a, b) =>
320           +a.comment.removed - +b.comment.removed ||
321           +a.comment.deleted - +b.comment.deleted ||
322           b.comment.score - a.comment.score
323       );
324     } else if (this.state.commentSort == CommentSortType.New) {
325       tree.sort(
326         (a, b) =>
327           +a.comment.removed - +b.comment.removed ||
328           +a.comment.deleted - +b.comment.deleted ||
329           b.comment.published.localeCompare(a.comment.published)
330       );
331     } else if (this.state.commentSort == CommentSortType.Old) {
332       tree.sort(
333         (a, b) =>
334           +a.comment.removed - +b.comment.removed ||
335           +a.comment.deleted - +b.comment.deleted ||
336           a.comment.published.localeCompare(b.comment.published)
337       );
338     } else if (this.state.commentSort == CommentSortType.Hot) {
339       tree.sort(
340         (a, b) =>
341           +a.comment.removed - +b.comment.removed ||
342           +a.comment.deleted - +b.comment.deleted ||
343           hotRank(b.comment) - hotRank(a.comment)
344       );
345     }
346
347     for (let node of tree) {
348       this.sortTree(node.children);
349     }
350   }
351
352   commentsTree() {
353     let nodes = this.buildCommentsTree();
354     return (
355       <div>
356         <CommentNodes
357           nodes={nodes}
358           locked={this.state.post.locked}
359           moderators={this.state.moderators}
360           admins={this.state.admins}
361           postCreatorId={this.state.post.creator_id}
362         />
363       </div>
364     );
365   }
366
367   parseMessage(msg: WebSocketJsonResponse) {
368     console.log(msg);
369     let res = wsJsonToRes(msg);
370     if (msg.error) {
371       toast(i18n.t(msg.error), 'danger');
372       return;
373     } else if (msg.reconnect) {
374       WebSocketService.Instance.getPost({
375         id: Number(this.props.match.params.id),
376       });
377     } else if (res.op == UserOperation.GetPost) {
378       let data = res.data as GetPostResponse;
379       this.state.post = data.post;
380       this.state.comments = data.comments;
381       this.state.community = data.community;
382       this.state.moderators = data.moderators;
383       this.state.admins = data.admins;
384       this.state.online = data.online;
385       this.state.loading = false;
386       document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
387
388       // Get cross-posts
389       if (this.state.post.url) {
390         let form: SearchForm = {
391           q: this.state.post.url,
392           type_: SearchType[SearchType.Url],
393           sort: SortType[SortType.TopAll],
394           page: 1,
395           limit: 6,
396         };
397         WebSocketService.Instance.search(form);
398       }
399
400       this.setState(this.state);
401     } else if (res.op == UserOperation.CreateComment) {
402       let data = res.data as CommentResponse;
403
404       // Necessary since it might be a user reply
405       if (data.recipient_ids.length == 0) {
406         this.state.comments.unshift(data.comment);
407         this.setState(this.state);
408       }
409     } else if (res.op == UserOperation.EditComment) {
410       let data = res.data as CommentResponse;
411       let found = this.state.comments.find(c => c.id == data.comment.id);
412       if (found) {
413         found.content = data.comment.content;
414         found.updated = data.comment.updated;
415         found.removed = data.comment.removed;
416         found.deleted = data.comment.deleted;
417         found.upvotes = data.comment.upvotes;
418         found.downvotes = data.comment.downvotes;
419         found.score = data.comment.score;
420         found.read = data.comment.read;
421
422         this.setState(this.state);
423       }
424     } else if (res.op == UserOperation.SaveComment) {
425       let data = res.data as CommentResponse;
426       let found = this.state.comments.find(c => c.id == data.comment.id);
427       if (found) {
428         found.saved = data.comment.saved;
429         this.setState(this.state);
430       }
431     } else if (res.op == UserOperation.CreateCommentLike) {
432       let data = res.data as CommentResponse;
433       let found: Comment = this.state.comments.find(
434         c => c.id === data.comment.id
435       );
436       if (found) {
437         found.score = data.comment.score;
438         found.upvotes = data.comment.upvotes;
439         found.downvotes = data.comment.downvotes;
440         if (data.comment.my_vote !== null) {
441           found.my_vote = data.comment.my_vote;
442           found.upvoteLoading = false;
443           found.downvoteLoading = false;
444         }
445       }
446       this.setState(this.state);
447     } else if (res.op == UserOperation.CreatePostLike) {
448       let data = res.data as PostResponse;
449       this.state.post.score = data.post.score;
450       this.state.post.upvotes = data.post.upvotes;
451       this.state.post.downvotes = data.post.downvotes;
452       if (data.post.my_vote !== null) {
453         this.state.post.my_vote = data.post.my_vote;
454         this.state.post.upvoteLoading = false;
455         this.state.post.downvoteLoading = false;
456       }
457
458       this.setState(this.state);
459     } else if (res.op == UserOperation.EditPost) {
460       let data = res.data as PostResponse;
461       this.state.post = data.post;
462       this.setState(this.state);
463     } else if (res.op == UserOperation.SavePost) {
464       let data = res.data as PostResponse;
465       this.state.post = data.post;
466       this.setState(this.state);
467     } else if (res.op == UserOperation.EditCommunity) {
468       let data = res.data as CommunityResponse;
469       this.state.community = data.community;
470       this.state.post.community_id = data.community.id;
471       this.state.post.community_name = data.community.name;
472       this.setState(this.state);
473     } else if (res.op == UserOperation.FollowCommunity) {
474       let data = res.data as CommunityResponse;
475       this.state.community.subscribed = data.community.subscribed;
476       this.state.community.number_of_subscribers =
477         data.community.number_of_subscribers;
478       this.setState(this.state);
479     } else if (res.op == UserOperation.BanFromCommunity) {
480       let data = res.data as BanFromCommunityResponse;
481       this.state.comments
482         .filter(c => c.creator_id == data.user.id)
483         .forEach(c => (c.banned_from_community = data.banned));
484       if (this.state.post.creator_id == data.user.id) {
485         this.state.post.banned_from_community = data.banned;
486       }
487       this.setState(this.state);
488     } else if (res.op == UserOperation.AddModToCommunity) {
489       let data = res.data as AddModToCommunityResponse;
490       this.state.moderators = data.moderators;
491       this.setState(this.state);
492     } else if (res.op == UserOperation.BanUser) {
493       let data = res.data as BanUserResponse;
494       this.state.comments
495         .filter(c => c.creator_id == data.user.id)
496         .forEach(c => (c.banned = data.banned));
497       if (this.state.post.creator_id == data.user.id) {
498         this.state.post.banned = data.banned;
499       }
500       this.setState(this.state);
501     } else if (res.op == UserOperation.AddAdmin) {
502       let data = res.data as AddAdminResponse;
503       this.state.admins = data.admins;
504       this.setState(this.state);
505     } else if (res.op == UserOperation.Search) {
506       let data = res.data as SearchResponse;
507       this.state.crossPosts = data.posts.filter(
508         p => p.id != this.state.post.id
509       );
510       this.setState(this.state);
511     } else if (res.op == UserOperation.TransferSite) {
512       let data = res.data as GetSiteResponse;
513
514       this.state.admins = data.admins;
515       this.setState(this.state);
516     } else if (res.op == UserOperation.TransferCommunity) {
517       let data = res.data as GetCommunityResponse;
518       this.state.community = data.community;
519       this.state.moderators = data.moderators;
520       this.state.admins = data.admins;
521       this.setState(this.state);
522     }
523   }
524 }