]> Untitled Git - lemmy.git/blob - ui/src/components/post.tsx
Fixing issue with multiple notifications.
[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   CreatePostLikeResponse,
15   CommunityUser,
16   CommunityResponse,
17   CommentNode as CommentNodeI,
18   BanFromCommunityResponse,
19   BanUserResponse,
20   AddModToCommunityResponse,
21   AddAdminResponse,
22   UserView,
23   SearchType,
24   SortType,
25   SearchForm,
26   GetPostForm,
27   SearchResponse,
28   GetSiteResponse,
29   GetCommunityResponse,
30   WebSocketJsonResponse,
31 } from '../interfaces';
32 import { WebSocketService, UserService } from '../services';
33 import { wsJsonToRes, hotRank, toast } from '../utils';
34 import { PostListing } from './post-listing';
35 import { PostListings } from './post-listings';
36 import { Sidebar } from './sidebar';
37 import { CommentForm } from './comment-form';
38 import { CommentNodes } from './comment-nodes';
39 import autosize from 'autosize';
40 import { i18n } from '../i18next';
41 import { T } from 'inferno-i18next';
42
43 interface PostState {
44   post: PostI;
45   comments: Array<Comment>;
46   commentSort: CommentSortType;
47   community: Community;
48   moderators: Array<CommunityUser>;
49   admins: Array<UserView>;
50   online: number;
51   scrolled?: boolean;
52   scrolled_comment_id?: number;
53   loading: boolean;
54   crossPosts: Array<PostI>;
55 }
56
57 export class Post extends Component<any, PostState> {
58   private subscription: Subscription;
59   private emptyState: PostState = {
60     post: null,
61     comments: [],
62     commentSort: CommentSortType.Hot,
63     community: null,
64     moderators: [],
65     admins: [],
66     online: null,
67     scrolled: false,
68     loading: true,
69     crossPosts: [],
70   };
71
72   constructor(props: any, context: any) {
73     super(props, context);
74
75     this.state = this.emptyState;
76
77     let postId = Number(this.props.match.params.id);
78     if (this.props.match.params.comment_id) {
79       this.state.scrolled_comment_id = this.props.match.params.comment_id;
80     }
81
82     this.subscription = WebSocketService.Instance.subject
83       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
84       .subscribe(
85         msg => this.parseMessage(msg),
86         err => console.error(err),
87         () => console.log('complete')
88       );
89
90     let form: GetPostForm = {
91       id: postId,
92     };
93     WebSocketService.Instance.getPost(form);
94   }
95
96   componentWillUnmount() {
97     this.subscription.unsubscribe();
98   }
99
100   componentDidMount() {
101     autosize(document.querySelectorAll('textarea'));
102   }
103
104   componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
105     if (
106       this.state.scrolled_comment_id &&
107       !this.state.scrolled &&
108       lastState.comments.length > 0
109     ) {
110       var elmnt = document.getElementById(
111         `comment-${this.state.scrolled_comment_id}`
112       );
113       elmnt.scrollIntoView();
114       elmnt.classList.add('mark');
115       this.state.scrolled = true;
116       this.markScrolledAsRead(this.state.scrolled_comment_id);
117     }
118
119     // Necessary if you are on a post and you click another post (same route)
120     if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
121       // Couldnt get a refresh working. This does for now.
122       location.reload();
123
124       // let currentId = this.props.match.params.id;
125       // WebSocketService.Instance.getPost(currentId);
126       // this.context.router.history.push('/sponsors');
127       // this.context.refresh();
128       // this.context.router.history.push(_lastProps.location.pathname);
129     }
130   }
131
132   markScrolledAsRead(commentId: number) {
133     let found = this.state.comments.find(c => c.id == commentId);
134     let parent = this.state.comments.find(c => found.parent_id == c.id);
135     let parent_user_id = parent
136       ? parent.creator_id
137       : this.state.post.creator_id;
138
139     if (
140       UserService.Instance.user &&
141       UserService.Instance.user.id == parent_user_id
142     ) {
143       let form: CommentFormI = {
144         content: found.content,
145         edit_id: found.id,
146         creator_id: found.creator_id,
147         post_id: found.post_id,
148         parent_id: found.parent_id,
149         read: true,
150         auth: null,
151       };
152       WebSocketService.Instance.editComment(form);
153     }
154   }
155
156   render() {
157     return (
158       <div class="container">
159         {this.state.loading ? (
160           <h5>
161             <svg class="icon icon-spinner spin">
162               <use xlinkHref="#icon-spinner"></use>
163             </svg>
164           </h5>
165         ) : (
166           <div class="row">
167             <div class="col-12 col-md-8 mb-3">
168               <PostListing
169                 post={this.state.post}
170                 showBody
171                 showCommunity
172                 moderators={this.state.moderators}
173                 admins={this.state.admins}
174               />
175               {this.state.crossPosts.length > 0 && (
176                 <>
177                   <div class="my-1 text-muted small font-weight-bold">
178                     <T i18nKey="cross_posts">#</T>
179                   </div>
180                   <PostListings showCommunity posts={this.state.crossPosts} />
181                 </>
182               )}
183               <div className="mb-2" />
184               <CommentForm
185                 postId={this.state.post.id}
186                 disabled={this.state.post.locked}
187               />
188               {this.state.comments.length > 0 && this.sortRadios()}
189               {this.commentsTree()}
190             </div>
191             <div class="col-12 col-sm-12 col-md-4">
192               {this.state.comments.length > 0 && this.newComments()}
193               {this.sidebar()}
194             </div>
195           </div>
196         )}
197       </div>
198     );
199   }
200
201   sortRadios() {
202     return (
203       <div class="btn-group btn-group-toggle mb-3">
204         <label
205           className={`btn btn-sm btn-secondary pointer ${this.state
206             .commentSort === CommentSortType.Hot && 'active'}`}
207         >
208           {i18n.t('hot')}
209           <input
210             type="radio"
211             value={CommentSortType.Hot}
212             checked={this.state.commentSort === CommentSortType.Hot}
213             onChange={linkEvent(this, this.handleCommentSortChange)}
214           />
215         </label>
216         <label
217           className={`btn btn-sm btn-secondary pointer ${this.state
218             .commentSort === CommentSortType.Top && 'active'}`}
219         >
220           {i18n.t('top')}
221           <input
222             type="radio"
223             value={CommentSortType.Top}
224             checked={this.state.commentSort === CommentSortType.Top}
225             onChange={linkEvent(this, this.handleCommentSortChange)}
226           />
227         </label>
228         <label
229           className={`btn btn-sm btn-secondary pointer ${this.state
230             .commentSort === CommentSortType.New && 'active'}`}
231         >
232           {i18n.t('new')}
233           <input
234             type="radio"
235             value={CommentSortType.New}
236             checked={this.state.commentSort === CommentSortType.New}
237             onChange={linkEvent(this, this.handleCommentSortChange)}
238           />
239         </label>
240         <label
241           className={`btn btn-sm btn-secondary pointer ${this.state
242             .commentSort === CommentSortType.Old && 'active'}`}
243         >
244           {i18n.t('old')}
245           <input
246             type="radio"
247             value={CommentSortType.Old}
248             checked={this.state.commentSort === CommentSortType.Old}
249             onChange={linkEvent(this, this.handleCommentSortChange)}
250           />
251         </label>
252       </div>
253     );
254   }
255
256   newComments() {
257     return (
258       <div class="d-none d-md-block new-comments mb-3 card border-secondary">
259         <div class="card-body small">
260           <h6>
261             <T i18nKey="recent_comments">#</T>
262           </h6>
263           {this.state.comments.map(comment => (
264             <CommentNodes
265               nodes={[{ comment: comment }]}
266               noIndent
267               locked={this.state.post.locked}
268               moderators={this.state.moderators}
269               admins={this.state.admins}
270               postCreatorId={this.state.post.creator_id}
271             />
272           ))}
273         </div>
274       </div>
275     );
276   }
277
278   sidebar() {
279     return (
280       <div class="mb-3">
281         <Sidebar
282           community={this.state.community}
283           moderators={this.state.moderators}
284           admins={this.state.admins}
285           online={this.state.online}
286         />
287       </div>
288     );
289   }
290
291   handleCommentSortChange(i: Post, event: any) {
292     i.state.commentSort = Number(event.target.value);
293     i.setState(i.state);
294   }
295
296   private buildCommentsTree(): Array<CommentNodeI> {
297     let map = new Map<number, CommentNodeI>();
298     for (let comment of this.state.comments) {
299       let node: CommentNodeI = {
300         comment: comment,
301         children: [],
302       };
303       map.set(comment.id, { ...node });
304     }
305     let tree: Array<CommentNodeI> = [];
306     for (let comment of this.state.comments) {
307       if (comment.parent_id) {
308         map.get(comment.parent_id).children.push(map.get(comment.id));
309       } else {
310         tree.push(map.get(comment.id));
311       }
312     }
313
314     this.sortTree(tree);
315
316     return tree;
317   }
318
319   sortTree(tree: Array<CommentNodeI>) {
320     // First, put removed and deleted comments at the bottom, then do your other sorts
321     if (this.state.commentSort == CommentSortType.Top) {
322       tree.sort(
323         (a, b) =>
324           +a.comment.removed - +b.comment.removed ||
325           +a.comment.deleted - +b.comment.deleted ||
326           b.comment.score - a.comment.score
327       );
328     } else if (this.state.commentSort == CommentSortType.New) {
329       tree.sort(
330         (a, b) =>
331           +a.comment.removed - +b.comment.removed ||
332           +a.comment.deleted - +b.comment.deleted ||
333           b.comment.published.localeCompare(a.comment.published)
334       );
335     } else if (this.state.commentSort == CommentSortType.Old) {
336       tree.sort(
337         (a, b) =>
338           +a.comment.removed - +b.comment.removed ||
339           +a.comment.deleted - +b.comment.deleted ||
340           a.comment.published.localeCompare(b.comment.published)
341       );
342     } else if (this.state.commentSort == CommentSortType.Hot) {
343       tree.sort(
344         (a, b) =>
345           +a.comment.removed - +b.comment.removed ||
346           +a.comment.deleted - +b.comment.deleted ||
347           hotRank(b.comment) - hotRank(a.comment)
348       );
349     }
350
351     for (let node of tree) {
352       this.sortTree(node.children);
353     }
354   }
355
356   commentsTree() {
357     let nodes = this.buildCommentsTree();
358     return (
359       <div>
360         <CommentNodes
361           nodes={nodes}
362           locked={this.state.post.locked}
363           moderators={this.state.moderators}
364           admins={this.state.admins}
365           postCreatorId={this.state.post.creator_id}
366         />
367       </div>
368     );
369   }
370
371   parseMessage(msg: WebSocketJsonResponse) {
372     console.log(msg);
373     let res = wsJsonToRes(msg);
374     if (msg.error) {
375       toast(i18n.t(msg.error), 'danger');
376       return;
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 a user might receive a comment reply on another thread
405       if (data.comment.post_id == this.state.post.id) {
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 CreatePostLikeResponse;
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 }