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