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