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