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