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