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