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