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