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