]> Untitled Git - lemmy.git/blob - ui/src/components/post.tsx
Merge branch 'federation' into dev_1
[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       found.content = data.comment.content;
413       found.updated = data.comment.updated;
414       found.removed = data.comment.removed;
415       found.deleted = data.comment.deleted;
416       found.upvotes = data.comment.upvotes;
417       found.downvotes = data.comment.downvotes;
418       found.score = data.comment.score;
419       found.read = data.comment.read;
420
421       this.setState(this.state);
422     } else if (res.op == UserOperation.SaveComment) {
423       let data = res.data as CommentResponse;
424       let found = this.state.comments.find(c => c.id == data.comment.id);
425       found.saved = data.comment.saved;
426       this.setState(this.state);
427     } else if (res.op == UserOperation.CreateCommentLike) {
428       let data = res.data as CommentResponse;
429       let found: Comment = this.state.comments.find(
430         c => c.id === data.comment.id
431       );
432       found.score = data.comment.score;
433       found.upvotes = data.comment.upvotes;
434       found.downvotes = data.comment.downvotes;
435       if (data.comment.my_vote !== null) {
436         found.my_vote = data.comment.my_vote;
437         found.upvoteLoading = false;
438         found.downvoteLoading = false;
439       }
440       this.setState(this.state);
441     } else if (res.op == UserOperation.CreatePostLike) {
442       let data = res.data as PostResponse;
443       this.state.post.score = data.post.score;
444       this.state.post.upvotes = data.post.upvotes;
445       this.state.post.downvotes = data.post.downvotes;
446       if (data.post.my_vote !== null) {
447         this.state.post.my_vote = data.post.my_vote;
448         this.state.post.upvoteLoading = false;
449         this.state.post.downvoteLoading = false;
450       }
451
452       this.setState(this.state);
453     } else if (res.op == UserOperation.EditPost) {
454       let data = res.data as PostResponse;
455       this.state.post = data.post;
456       this.setState(this.state);
457     } else if (res.op == UserOperation.SavePost) {
458       let data = res.data as PostResponse;
459       this.state.post = data.post;
460       this.setState(this.state);
461     } else if (res.op == UserOperation.EditCommunity) {
462       let data = res.data as CommunityResponse;
463       this.state.community = data.community;
464       this.state.post.community_id = data.community.id;
465       this.state.post.community_name = data.community.name;
466       this.setState(this.state);
467     } else if (res.op == UserOperation.FollowCommunity) {
468       let data = res.data as CommunityResponse;
469       this.state.community.subscribed = data.community.subscribed;
470       this.state.community.number_of_subscribers =
471         data.community.number_of_subscribers;
472       this.setState(this.state);
473     } else if (res.op == UserOperation.BanFromCommunity) {
474       let data = res.data as BanFromCommunityResponse;
475       this.state.comments
476         .filter(c => c.creator_id == data.user.id)
477         .forEach(c => (c.banned_from_community = data.banned));
478       if (this.state.post.creator_id == data.user.id) {
479         this.state.post.banned_from_community = data.banned;
480       }
481       this.setState(this.state);
482     } else if (res.op == UserOperation.AddModToCommunity) {
483       let data = res.data as AddModToCommunityResponse;
484       this.state.moderators = data.moderators;
485       this.setState(this.state);
486     } else if (res.op == UserOperation.BanUser) {
487       let data = res.data as BanUserResponse;
488       this.state.comments
489         .filter(c => c.creator_id == data.user.id)
490         .forEach(c => (c.banned = data.banned));
491       if (this.state.post.creator_id == data.user.id) {
492         this.state.post.banned = data.banned;
493       }
494       this.setState(this.state);
495     } else if (res.op == UserOperation.AddAdmin) {
496       let data = res.data as AddAdminResponse;
497       this.state.admins = data.admins;
498       this.setState(this.state);
499     } else if (res.op == UserOperation.Search) {
500       let data = res.data as SearchResponse;
501       this.state.crossPosts = data.posts.filter(
502         p => p.id != this.state.post.id
503       );
504       this.setState(this.state);
505     } else if (res.op == UserOperation.TransferSite) {
506       let data = res.data as GetSiteResponse;
507
508       this.state.admins = data.admins;
509       this.setState(this.state);
510     } else if (res.op == UserOperation.TransferCommunity) {
511       let data = res.data as GetCommunityResponse;
512       this.state.community = data.community;
513       this.state.moderators = data.moderators;
514       this.state.admins = data.admins;
515       this.setState(this.state);
516     }
517   }
518 }