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