]> Untitled Git - lemmy-ui.git/blob - src/shared/components/post.tsx
On component unmounting, unset the window iso data, to force a refresh.
[lemmy-ui.git] / src / shared / components / post.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { HtmlTags } from './html-tags';
3 import { Subscription } from 'rxjs';
4 import {
5   UserOperation,
6   Post as PostI,
7   GetPostResponse,
8   PostResponse,
9   MarkCommentAsReadForm,
10   CommentResponse,
11   CommunityResponse,
12   CommentNode as CommentNodeI,
13   BanFromCommunityResponse,
14   BanUserResponse,
15   AddModToCommunityResponse,
16   AddAdminResponse,
17   SearchType,
18   SortType,
19   SearchForm,
20   GetPostForm,
21   SearchResponse,
22   GetSiteResponse,
23   GetCommunityResponse,
24   WebSocketJsonResponse,
25   ListCategoriesResponse,
26   Category,
27 } from 'lemmy-js-client';
28 import { CommentSortType, CommentViewType } from '../interfaces';
29 import { WebSocketService, UserService } from '../services';
30 import {
31   wsJsonToRes,
32   toast,
33   editCommentRes,
34   saveCommentRes,
35   createCommentLikeRes,
36   createPostLikeRes,
37   commentsToFlatNodes,
38   setupTippy,
39   setIsoData,
40   getIdFromProps,
41   getCommentIdFromProps,
42   wsSubscribe,
43   setAuth,
44   lemmyHttp,
45   isBrowser,
46   previewLines,
47   isImage,
48 } from '../utils';
49 import { PostListing } from './post-listing';
50 import { Sidebar } from './sidebar';
51 import { CommentForm } from './comment-form';
52 import { CommentNodes } from './comment-nodes';
53 import autosize from 'autosize';
54 import { i18n } from '../i18next';
55
56 interface PostState {
57   postRes: GetPostResponse;
58   postId: number;
59   commentId?: number;
60   commentSort: CommentSortType;
61   commentViewType: CommentViewType;
62   scrolled?: boolean;
63   loading: boolean;
64   crossPosts: PostI[];
65   siteRes: GetSiteResponse;
66   categories: Category[];
67 }
68
69 export class Post extends Component<any, PostState> {
70   private subscription: Subscription;
71   private isoData = setIsoData(this.context);
72   private emptyState: PostState = {
73     postRes: null,
74     postId: getIdFromProps(this.props),
75     commentId: getCommentIdFromProps(this.props),
76     commentSort: CommentSortType.Hot,
77     commentViewType: CommentViewType.Tree,
78     scrolled: false,
79     loading: true,
80     crossPosts: [],
81     siteRes: this.isoData.site,
82     categories: [],
83   };
84
85   constructor(props: any, context: any) {
86     super(props, context);
87
88     this.state = this.emptyState;
89
90     this.parseMessage = this.parseMessage.bind(this);
91     this.subscription = wsSubscribe(this.parseMessage);
92
93     // Only fetch the data if coming from another route
94     if (this.isoData.path == this.context.router.route.match.url) {
95       this.state.postRes = this.isoData.routeData[0];
96       this.state.categories = this.isoData.routeData[1].categories;
97       this.state.loading = false;
98
99       if (isBrowser() && this.state.commentId) {
100         this.scrollCommentIntoView();
101       }
102     } else {
103       this.fetchPost();
104       WebSocketService.Instance.listCategories();
105     }
106   }
107
108   fetchPost() {
109     let form: GetPostForm = {
110       id: this.state.postId,
111     };
112     WebSocketService.Instance.getPost(form);
113   }
114
115   static fetchInitialData(auth: string, path: string): Promise<any>[] {
116     let pathSplit = path.split('/');
117     let promises: Promise<any>[] = [];
118
119     let id = Number(pathSplit[2]);
120
121     let postForm: GetPostForm = {
122       id,
123     };
124     setAuth(postForm, auth);
125
126     promises.push(lemmyHttp.getPost(postForm));
127     promises.push(lemmyHttp.listCategories());
128
129     return promises;
130   }
131
132   componentWillUnmount() {
133     this.subscription.unsubscribe();
134     window.isoData.path = undefined;
135   }
136
137   componentDidMount() {
138     WebSocketService.Instance.postJoin({ post_id: this.state.postId });
139     autosize(document.querySelectorAll('textarea'));
140   }
141
142   componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
143     if (
144       this.state.commentId &&
145       !this.state.scrolled &&
146       lastState.postRes &&
147       lastState.postRes.comments.length > 0
148     ) {
149       this.scrollCommentIntoView();
150     }
151
152     // Necessary if you are on a post and you click another post (same route)
153     if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
154       // TODO Couldnt get a refresh working. This does for now.
155       location.reload();
156
157       // let currentId = this.props.match.params.id;
158       // WebSocketService.Instance.getPost(currentId);
159       // this.context.refresh();
160       // this.context.router.history.push(_lastProps.location.pathname);
161     }
162   }
163
164   scrollCommentIntoView() {
165     var elmnt = document.getElementById(`comment-${this.state.commentId}`);
166     elmnt.scrollIntoView();
167     elmnt.classList.add('mark');
168     this.state.scrolled = true;
169     this.markScrolledAsRead(this.state.commentId);
170   }
171
172   markScrolledAsRead(commentId: number) {
173     let found = this.state.postRes.comments.find(c => c.id == commentId);
174     let parent = this.state.postRes.comments.find(c => found.parent_id == c.id);
175     let parent_user_id = parent
176       ? parent.creator_id
177       : this.state.postRes.post.creator_id;
178
179     if (
180       UserService.Instance.user &&
181       UserService.Instance.user.id == parent_user_id
182     ) {
183       let form: MarkCommentAsReadForm = {
184         edit_id: found.id,
185         read: true,
186         auth: null,
187       };
188       WebSocketService.Instance.markCommentAsRead(form);
189       UserService.Instance.unreadCountSub.next(
190         UserService.Instance.unreadCountSub.value - 1
191       );
192     }
193   }
194
195   get documentTitle(): string {
196     return `${this.state.postRes.post.name} - ${this.state.siteRes.site.name}`;
197   }
198
199   get imageTag(): string {
200     return (
201       this.state.postRes.post.thumbnail_url ||
202       (this.state.postRes.post.url
203         ? isImage(this.state.postRes.post.url)
204           ? this.state.postRes.post.url
205           : undefined
206         : undefined)
207     );
208   }
209
210   get descriptionTag(): string {
211     return this.state.postRes.post.body
212       ? previewLines(this.state.postRes.post.body)
213       : undefined;
214   }
215
216   render() {
217     return (
218       <div class="container">
219         {this.state.loading ? (
220           <h5>
221             <svg class="icon icon-spinner spin">
222               <use xlinkHref="#icon-spinner"></use>
223             </svg>
224           </h5>
225         ) : (
226           <div class="row">
227             <div class="col-12 col-md-8 mb-3">
228               <HtmlTags
229                 title={this.documentTitle}
230                 path={this.context.router.route.match.url}
231                 image={this.imageTag}
232                 description={this.descriptionTag}
233               />
234               <PostListing
235                 post={this.state.postRes.post}
236                 showBody
237                 showCommunity
238                 moderators={this.state.postRes.moderators}
239                 admins={this.state.siteRes.admins}
240                 enableDownvotes={this.state.siteRes.site.enable_downvotes}
241                 enableNsfw={this.state.siteRes.site.enable_nsfw}
242               />
243               <div className="mb-2" />
244               <CommentForm
245                 postId={this.state.postId}
246                 disabled={this.state.postRes.post.locked}
247               />
248               {this.state.postRes.comments.length > 0 && this.sortRadios()}
249               {this.state.commentViewType == CommentViewType.Tree &&
250                 this.commentsTree()}
251               {this.state.commentViewType == CommentViewType.Chat &&
252                 this.commentsFlat()}
253             </div>
254             <div class="col-12 col-sm-12 col-md-4">{this.sidebar()}</div>
255           </div>
256         )}
257       </div>
258     );
259   }
260
261   sortRadios() {
262     return (
263       <>
264         <div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
265           <label
266             className={`btn btn-outline-secondary pointer ${
267               this.state.commentSort === CommentSortType.Hot && 'active'
268             }`}
269           >
270             {i18n.t('hot')}
271             <input
272               type="radio"
273               value={CommentSortType.Hot}
274               checked={this.state.commentSort === CommentSortType.Hot}
275               onChange={linkEvent(this, this.handleCommentSortChange)}
276             />
277           </label>
278           <label
279             className={`btn btn-outline-secondary pointer ${
280               this.state.commentSort === CommentSortType.Top && 'active'
281             }`}
282           >
283             {i18n.t('top')}
284             <input
285               type="radio"
286               value={CommentSortType.Top}
287               checked={this.state.commentSort === CommentSortType.Top}
288               onChange={linkEvent(this, this.handleCommentSortChange)}
289             />
290           </label>
291           <label
292             className={`btn btn-outline-secondary pointer ${
293               this.state.commentSort === CommentSortType.New && 'active'
294             }`}
295           >
296             {i18n.t('new')}
297             <input
298               type="radio"
299               value={CommentSortType.New}
300               checked={this.state.commentSort === CommentSortType.New}
301               onChange={linkEvent(this, this.handleCommentSortChange)}
302             />
303           </label>
304           <label
305             className={`btn btn-outline-secondary pointer ${
306               this.state.commentSort === CommentSortType.Old && 'active'
307             }`}
308           >
309             {i18n.t('old')}
310             <input
311               type="radio"
312               value={CommentSortType.Old}
313               checked={this.state.commentSort === CommentSortType.Old}
314               onChange={linkEvent(this, this.handleCommentSortChange)}
315             />
316           </label>
317         </div>
318         <div class="btn-group btn-group-toggle flex-wrap mb-2">
319           <label
320             className={`btn btn-outline-secondary pointer ${
321               this.state.commentViewType === CommentViewType.Chat && 'active'
322             }`}
323           >
324             {i18n.t('chat')}
325             <input
326               type="radio"
327               value={CommentViewType.Chat}
328               checked={this.state.commentViewType === CommentViewType.Chat}
329               onChange={linkEvent(this, this.handleCommentViewTypeChange)}
330             />
331           </label>
332         </div>
333       </>
334     );
335   }
336
337   commentsFlat() {
338     return (
339       <div>
340         <CommentNodes
341           nodes={commentsToFlatNodes(this.state.postRes.comments)}
342           noIndent
343           locked={this.state.postRes.post.locked}
344           moderators={this.state.postRes.moderators}
345           admins={this.state.siteRes.admins}
346           postCreatorId={this.state.postRes.post.creator_id}
347           showContext
348           enableDownvotes={this.state.siteRes.site.enable_downvotes}
349           sort={this.state.commentSort}
350         />
351       </div>
352     );
353   }
354
355   sidebar() {
356     return (
357       <div class="mb-3">
358         <Sidebar
359           community={this.state.postRes.community}
360           moderators={this.state.postRes.moderators}
361           admins={this.state.siteRes.admins}
362           online={this.state.postRes.online}
363           enableNsfw={this.state.siteRes.site.enable_nsfw}
364           showIcon
365           categories={this.state.categories}
366         />
367       </div>
368     );
369   }
370
371   handleCommentSortChange(i: Post, event: any) {
372     i.state.commentSort = Number(event.target.value);
373     i.state.commentViewType = CommentViewType.Tree;
374     i.setState(i.state);
375   }
376
377   handleCommentViewTypeChange(i: Post, event: any) {
378     i.state.commentViewType = Number(event.target.value);
379     i.state.commentSort = CommentSortType.New;
380     i.setState(i.state);
381   }
382
383   buildCommentsTree(): CommentNodeI[] {
384     let map = new Map<number, CommentNodeI>();
385     for (let comment of this.state.postRes.comments) {
386       let node: CommentNodeI = {
387         comment: comment,
388         children: [],
389       };
390       map.set(comment.id, { ...node });
391     }
392     let tree: CommentNodeI[] = [];
393     for (let comment of this.state.postRes.comments) {
394       let child = map.get(comment.id);
395       if (comment.parent_id) {
396         let parent_ = map.get(comment.parent_id);
397         parent_.children.push(child);
398       } else {
399         tree.push(child);
400       }
401
402       this.setDepth(child);
403     }
404
405     return tree;
406   }
407
408   setDepth(node: CommentNodeI, i: number = 0): void {
409     for (let child of node.children) {
410       child.comment.depth = i;
411       this.setDepth(child, i + 1);
412     }
413   }
414
415   commentsTree() {
416     let nodes = this.buildCommentsTree();
417     return (
418       <div>
419         <CommentNodes
420           nodes={nodes}
421           locked={this.state.postRes.post.locked}
422           moderators={this.state.postRes.moderators}
423           admins={this.state.siteRes.admins}
424           postCreatorId={this.state.postRes.post.creator_id}
425           sort={this.state.commentSort}
426           enableDownvotes={this.state.siteRes.site.enable_downvotes}
427         />
428       </div>
429     );
430   }
431
432   parseMessage(msg: WebSocketJsonResponse) {
433     console.log(msg);
434     let res = wsJsonToRes(msg);
435     if (msg.error) {
436       toast(i18n.t(msg.error), 'danger');
437       return;
438     } else if (msg.reconnect) {
439       let postId = Number(this.props.match.params.id);
440       WebSocketService.Instance.postJoin({ post_id: postId });
441       WebSocketService.Instance.getPost({
442         id: postId,
443       });
444     } else if (res.op == UserOperation.GetPost) {
445       let data = res.data as GetPostResponse;
446       this.state.postRes = data;
447       this.state.loading = false;
448
449       // Get cross-posts
450       if (this.state.postRes.post.url) {
451         let form: SearchForm = {
452           q: this.state.postRes.post.url,
453           type_: SearchType.Url,
454           sort: SortType.TopAll,
455           page: 1,
456           limit: 6,
457         };
458         WebSocketService.Instance.search(form);
459       }
460
461       this.setState(this.state);
462       setupTippy();
463     } else if (res.op == UserOperation.CreateComment) {
464       let data = res.data as CommentResponse;
465
466       // Necessary since it might be a user reply
467       if (data.recipient_ids.length == 0) {
468         this.state.postRes.comments.unshift(data.comment);
469         this.setState(this.state);
470       }
471     } else if (
472       res.op == UserOperation.EditComment ||
473       res.op == UserOperation.DeleteComment ||
474       res.op == UserOperation.RemoveComment
475     ) {
476       let data = res.data as CommentResponse;
477       editCommentRes(data, this.state.postRes.comments);
478       this.setState(this.state);
479     } else if (res.op == UserOperation.SaveComment) {
480       let data = res.data as CommentResponse;
481       saveCommentRes(data, this.state.postRes.comments);
482       this.setState(this.state);
483       setupTippy();
484     } else if (res.op == UserOperation.CreateCommentLike) {
485       let data = res.data as CommentResponse;
486       createCommentLikeRes(data, this.state.postRes.comments);
487       this.setState(this.state);
488     } else if (res.op == UserOperation.CreatePostLike) {
489       let data = res.data as PostResponse;
490       createPostLikeRes(data, this.state.postRes.post);
491       this.setState(this.state);
492     } else if (
493       res.op == UserOperation.EditPost ||
494       res.op == UserOperation.DeletePost ||
495       res.op == UserOperation.RemovePost ||
496       res.op == UserOperation.LockPost ||
497       res.op == UserOperation.StickyPost
498     ) {
499       let data = res.data as PostResponse;
500       this.state.postRes.post = data.post;
501       this.setState(this.state);
502       setupTippy();
503     } else if (res.op == UserOperation.SavePost) {
504       let data = res.data as PostResponse;
505       this.state.postRes.post = data.post;
506       this.setState(this.state);
507       setupTippy();
508     } else if (
509       res.op == UserOperation.EditCommunity ||
510       res.op == UserOperation.DeleteCommunity ||
511       res.op == UserOperation.RemoveCommunity
512     ) {
513       let data = res.data as CommunityResponse;
514       this.state.postRes.community = data.community;
515       this.state.postRes.post.community_id = data.community.id;
516       this.state.postRes.post.community_name = data.community.name;
517       this.setState(this.state);
518     } else if (res.op == UserOperation.FollowCommunity) {
519       let data = res.data as CommunityResponse;
520       this.state.postRes.community.subscribed = data.community.subscribed;
521       this.state.postRes.community.number_of_subscribers =
522         data.community.number_of_subscribers;
523       this.setState(this.state);
524     } else if (res.op == UserOperation.BanFromCommunity) {
525       let data = res.data as BanFromCommunityResponse;
526       this.state.postRes.comments
527         .filter(c => c.creator_id == data.user.id)
528         .forEach(c => (c.banned_from_community = data.banned));
529       if (this.state.postRes.post.creator_id == data.user.id) {
530         this.state.postRes.post.banned_from_community = data.banned;
531       }
532       this.setState(this.state);
533     } else if (res.op == UserOperation.AddModToCommunity) {
534       let data = res.data as AddModToCommunityResponse;
535       this.state.postRes.moderators = data.moderators;
536       this.setState(this.state);
537     } else if (res.op == UserOperation.BanUser) {
538       let data = res.data as BanUserResponse;
539       this.state.postRes.comments
540         .filter(c => c.creator_id == data.user.id)
541         .forEach(c => (c.banned = data.banned));
542       if (this.state.postRes.post.creator_id == data.user.id) {
543         this.state.postRes.post.banned = data.banned;
544       }
545       this.setState(this.state);
546     } else if (res.op == UserOperation.AddAdmin) {
547       let data = res.data as AddAdminResponse;
548       this.state.siteRes.admins = data.admins;
549       this.setState(this.state);
550     } else if (res.op == UserOperation.Search) {
551       let data = res.data as SearchResponse;
552       this.state.crossPosts = data.posts.filter(
553         p => p.id != Number(this.props.match.params.id)
554       );
555       if (this.state.crossPosts.length) {
556         this.state.postRes.post.duplicates = this.state.crossPosts;
557       }
558       this.setState(this.state);
559     } else if (res.op == UserOperation.TransferSite) {
560       let data = res.data as GetSiteResponse;
561       this.state.siteRes = data;
562       this.setState(this.state);
563     } else if (res.op == UserOperation.TransferCommunity) {
564       let data = res.data as GetCommunityResponse;
565       this.state.postRes.community = data.community;
566       this.state.postRes.moderators = data.moderators;
567       this.setState(this.state);
568     } else if (res.op == UserOperation.ListCategories) {
569       let data = res.data as ListCategoriesResponse;
570       this.state.categories = data.categories;
571       this.setState(this.state);
572     }
573   }
574 }