]> Untitled Git - lemmy.git/blob - ui/src/components/post.tsx
Merge pull request #498 from iav/docs4arm
[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 (res.op == UserOperation.GetPost) {
374       let data = res.data as GetPostResponse;
375       this.state.post = data.post;
376       this.state.comments = data.comments;
377       this.state.community = data.community;
378       this.state.moderators = data.moderators;
379       this.state.admins = data.admins;
380       this.state.online = data.online;
381       this.state.loading = false;
382       document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
383
384       // Get cross-posts
385       if (this.state.post.url) {
386         let form: SearchForm = {
387           q: this.state.post.url,
388           type_: SearchType[SearchType.Url],
389           sort: SortType[SortType.TopAll],
390           page: 1,
391           limit: 6,
392         };
393         WebSocketService.Instance.search(form);
394       }
395
396       this.setState(this.state);
397     } else if (res.op == UserOperation.CreateComment) {
398       let data = res.data as CommentResponse;
399
400       // Necessary since it might be a user reply
401       if (data.recipient_ids.length == 0) {
402         this.state.comments.unshift(data.comment);
403         this.setState(this.state);
404       }
405     } else if (res.op == UserOperation.EditComment) {
406       let data = res.data as CommentResponse;
407       let found = this.state.comments.find(c => c.id == data.comment.id);
408       found.content = data.comment.content;
409       found.updated = data.comment.updated;
410       found.removed = data.comment.removed;
411       found.deleted = data.comment.deleted;
412       found.upvotes = data.comment.upvotes;
413       found.downvotes = data.comment.downvotes;
414       found.score = data.comment.score;
415       found.read = data.comment.read;
416
417       this.setState(this.state);
418     } else if (res.op == UserOperation.SaveComment) {
419       let data = res.data as CommentResponse;
420       let found = this.state.comments.find(c => c.id == data.comment.id);
421       found.saved = data.comment.saved;
422       this.setState(this.state);
423     } else if (res.op == UserOperation.CreateCommentLike) {
424       let data = res.data as CommentResponse;
425       let found: Comment = this.state.comments.find(
426         c => c.id === data.comment.id
427       );
428       found.score = data.comment.score;
429       found.upvotes = data.comment.upvotes;
430       found.downvotes = data.comment.downvotes;
431       if (data.comment.my_vote !== null) {
432         found.my_vote = data.comment.my_vote;
433         found.upvoteLoading = false;
434         found.downvoteLoading = false;
435       }
436       this.setState(this.state);
437     } else if (res.op == UserOperation.CreatePostLike) {
438       let data = res.data as PostResponse;
439       this.state.post.score = data.post.score;
440       this.state.post.upvotes = data.post.upvotes;
441       this.state.post.downvotes = data.post.downvotes;
442       if (data.post.my_vote !== null) {
443         this.state.post.my_vote = data.post.my_vote;
444         this.state.post.upvoteLoading = false;
445         this.state.post.downvoteLoading = false;
446       }
447
448       this.setState(this.state);
449     } else if (res.op == UserOperation.EditPost) {
450       let data = res.data as PostResponse;
451       this.state.post = data.post;
452       this.setState(this.state);
453     } else if (res.op == UserOperation.SavePost) {
454       let data = res.data as PostResponse;
455       this.state.post = data.post;
456       this.setState(this.state);
457     } else if (res.op == UserOperation.EditCommunity) {
458       let data = res.data as CommunityResponse;
459       this.state.community = data.community;
460       this.state.post.community_id = data.community.id;
461       this.state.post.community_name = data.community.name;
462       this.setState(this.state);
463     } else if (res.op == UserOperation.FollowCommunity) {
464       let data = res.data as CommunityResponse;
465       this.state.community.subscribed = data.community.subscribed;
466       this.state.community.number_of_subscribers =
467         data.community.number_of_subscribers;
468       this.setState(this.state);
469     } else if (res.op == UserOperation.BanFromCommunity) {
470       let data = res.data as BanFromCommunityResponse;
471       this.state.comments
472         .filter(c => c.creator_id == data.user.id)
473         .forEach(c => (c.banned_from_community = data.banned));
474       if (this.state.post.creator_id == data.user.id) {
475         this.state.post.banned_from_community = data.banned;
476       }
477       this.setState(this.state);
478     } else if (res.op == UserOperation.AddModToCommunity) {
479       let data = res.data as AddModToCommunityResponse;
480       this.state.moderators = data.moderators;
481       this.setState(this.state);
482     } else if (res.op == UserOperation.BanUser) {
483       let data = res.data as BanUserResponse;
484       this.state.comments
485         .filter(c => c.creator_id == data.user.id)
486         .forEach(c => (c.banned = data.banned));
487       if (this.state.post.creator_id == data.user.id) {
488         this.state.post.banned = data.banned;
489       }
490       this.setState(this.state);
491     } else if (res.op == UserOperation.AddAdmin) {
492       let data = res.data as AddAdminResponse;
493       this.state.admins = data.admins;
494       this.setState(this.state);
495     } else if (res.op == UserOperation.Search) {
496       let data = res.data as SearchResponse;
497       this.state.crossPosts = data.posts.filter(
498         p => p.id != this.state.post.id
499       );
500       this.setState(this.state);
501     } else if (res.op == UserOperation.TransferSite) {
502       let data = res.data as GetSiteResponse;
503
504       this.state.admins = data.admins;
505       this.setState(this.state);
506     } else if (res.op == UserOperation.TransferCommunity) {
507       let data = res.data as GetCommunityResponse;
508       this.state.community = data.community;
509       this.state.moderators = data.moderators;
510       this.state.admins = data.admins;
511       this.setState(this.state);
512     }
513   }
514 }