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