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