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