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