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