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