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