]> Untitled Git - lemmy-ui.git/blob - src/shared/components/post.tsx
Adding support for join messages. Fixes #8
[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     WebSocketService.Instance.postJoin({ post_id: this.state.postId });
138     autosize(document.querySelectorAll('textarea'));
139   }
140
141   componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
142     if (
143       this.state.commentId &&
144       !this.state.scrolled &&
145       lastState.postRes &&
146       lastState.postRes.comments.length > 0
147     ) {
148       this.scrollCommentIntoView();
149     }
150
151     // Necessary if you are on a post and you click another post (same route)
152     if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
153       // TODO Couldnt get a refresh working. This does for now.
154       location.reload();
155
156       // let currentId = this.props.match.params.id;
157       // WebSocketService.Instance.getPost(currentId);
158       // this.context.router.history.push('/sponsors');
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       WebSocketService.Instance.getPost({
440         id: Number(this.props.match.params.id),
441       });
442     } else if (res.op == UserOperation.GetPost) {
443       let data = res.data as GetPostResponse;
444       this.state.postRes = data;
445       this.state.loading = false;
446
447       // Get cross-posts
448       if (this.state.postRes.post.url) {
449         let form: SearchForm = {
450           q: this.state.postRes.post.url,
451           type_: SearchType.Url,
452           sort: SortType.TopAll,
453           page: 1,
454           limit: 6,
455         };
456         WebSocketService.Instance.search(form);
457       }
458
459       this.setState(this.state);
460       setupTippy();
461     } else if (res.op == UserOperation.CreateComment) {
462       let data = res.data as CommentResponse;
463
464       // Necessary since it might be a user reply
465       if (data.recipient_ids.length == 0) {
466         this.state.postRes.comments.unshift(data.comment);
467         this.setState(this.state);
468       }
469     } else if (
470       res.op == UserOperation.EditComment ||
471       res.op == UserOperation.DeleteComment ||
472       res.op == UserOperation.RemoveComment
473     ) {
474       let data = res.data as CommentResponse;
475       editCommentRes(data, this.state.postRes.comments);
476       this.setState(this.state);
477     } else if (res.op == UserOperation.SaveComment) {
478       let data = res.data as CommentResponse;
479       saveCommentRes(data, this.state.postRes.comments);
480       this.setState(this.state);
481       setupTippy();
482     } else if (res.op == UserOperation.CreateCommentLike) {
483       let data = res.data as CommentResponse;
484       createCommentLikeRes(data, this.state.postRes.comments);
485       this.setState(this.state);
486     } else if (res.op == UserOperation.CreatePostLike) {
487       let data = res.data as PostResponse;
488       createPostLikeRes(data, this.state.postRes.post);
489       this.setState(this.state);
490     } else if (
491       res.op == UserOperation.EditPost ||
492       res.op == UserOperation.DeletePost ||
493       res.op == UserOperation.RemovePost ||
494       res.op == UserOperation.LockPost ||
495       res.op == UserOperation.StickyPost
496     ) {
497       let data = res.data as PostResponse;
498       this.state.postRes.post = data.post;
499       this.setState(this.state);
500       setupTippy();
501     } else if (res.op == UserOperation.SavePost) {
502       let data = res.data as PostResponse;
503       this.state.postRes.post = data.post;
504       this.setState(this.state);
505       setupTippy();
506     } else if (
507       res.op == UserOperation.EditCommunity ||
508       res.op == UserOperation.DeleteCommunity ||
509       res.op == UserOperation.RemoveCommunity
510     ) {
511       let data = res.data as CommunityResponse;
512       this.state.postRes.community = data.community;
513       this.state.postRes.post.community_id = data.community.id;
514       this.state.postRes.post.community_name = data.community.name;
515       this.setState(this.state);
516     } else if (res.op == UserOperation.FollowCommunity) {
517       let data = res.data as CommunityResponse;
518       this.state.postRes.community.subscribed = data.community.subscribed;
519       this.state.postRes.community.number_of_subscribers =
520         data.community.number_of_subscribers;
521       this.setState(this.state);
522     } else if (res.op == UserOperation.BanFromCommunity) {
523       let data = res.data as BanFromCommunityResponse;
524       this.state.postRes.comments
525         .filter(c => c.creator_id == data.user.id)
526         .forEach(c => (c.banned_from_community = data.banned));
527       if (this.state.postRes.post.creator_id == data.user.id) {
528         this.state.postRes.post.banned_from_community = data.banned;
529       }
530       this.setState(this.state);
531     } else if (res.op == UserOperation.AddModToCommunity) {
532       let data = res.data as AddModToCommunityResponse;
533       this.state.postRes.moderators = data.moderators;
534       this.setState(this.state);
535     } else if (res.op == UserOperation.BanUser) {
536       let data = res.data as BanUserResponse;
537       this.state.postRes.comments
538         .filter(c => c.creator_id == data.user.id)
539         .forEach(c => (c.banned = data.banned));
540       if (this.state.postRes.post.creator_id == data.user.id) {
541         this.state.postRes.post.banned = data.banned;
542       }
543       this.setState(this.state);
544     } else if (res.op == UserOperation.AddAdmin) {
545       let data = res.data as AddAdminResponse;
546       this.state.siteRes.admins = data.admins;
547       this.setState(this.state);
548     } else if (res.op == UserOperation.Search) {
549       let data = res.data as SearchResponse;
550       this.state.crossPosts = data.posts.filter(
551         p => p.id != Number(this.props.match.params.id)
552       );
553       if (this.state.crossPosts.length) {
554         this.state.postRes.post.duplicates = this.state.crossPosts;
555       }
556       this.setState(this.state);
557     } else if (res.op == UserOperation.TransferSite) {
558       let data = res.data as GetSiteResponse;
559       this.state.siteRes = data;
560       this.setState(this.state);
561     } else if (res.op == UserOperation.TransferCommunity) {
562       let data = res.data as GetCommunityResponse;
563       this.state.postRes.community = data.community;
564       this.state.postRes.moderators = data.moderators;
565       this.setState(this.state);
566     } else if (res.op == UserOperation.ListCategories) {
567       let data = res.data as ListCategoriesResponse;
568       this.state.categories = data.categories;
569       this.setState(this.state);
570     }
571   }
572 }