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