]> Untitled Git - lemmy.git/blob - ui/src/components/post.tsx
cf9e748652e5241ddcaf9ba84f64da0d4f259ba4
[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 mb-2">
215         <label
216           className={`btn btn-sm btn-secondary pointer ${
217             this.state.commentSort === CommentSortType.Hot && 'active'
218           }`}
219         >
220           {i18n.t('hot')}
221           <input
222             type="radio"
223             value={CommentSortType.Hot}
224             checked={this.state.commentSort === CommentSortType.Hot}
225             onChange={linkEvent(this, this.handleCommentSortChange)}
226           />
227         </label>
228         <label
229           className={`btn btn-sm btn-secondary pointer ${
230             this.state.commentSort === CommentSortType.Top && 'active'
231           }`}
232         >
233           {i18n.t('top')}
234           <input
235             type="radio"
236             value={CommentSortType.Top}
237             checked={this.state.commentSort === CommentSortType.Top}
238             onChange={linkEvent(this, this.handleCommentSortChange)}
239           />
240         </label>
241         <label
242           className={`btn btn-sm btn-secondary pointer ${
243             this.state.commentSort === CommentSortType.New && 'active'
244           }`}
245         >
246           {i18n.t('new')}
247           <input
248             type="radio"
249             value={CommentSortType.New}
250             checked={this.state.commentSort === CommentSortType.New}
251             onChange={linkEvent(this, this.handleCommentSortChange)}
252           />
253         </label>
254         <label
255           className={`btn btn-sm btn-secondary pointer ${
256             this.state.commentSort === CommentSortType.Old && 'active'
257           }`}
258         >
259           {i18n.t('old')}
260           <input
261             type="radio"
262             value={CommentSortType.Old}
263             checked={this.state.commentSort === CommentSortType.Old}
264             onChange={linkEvent(this, this.handleCommentSortChange)}
265           />
266         </label>
267       </div>
268     );
269   }
270
271   newComments() {
272     return (
273       <div class="d-none d-md-block new-comments mb-3 card border-secondary">
274         <div class="card-body small">
275           <h6>{i18n.t('recent_comments')}</h6>
276           <CommentNodes
277             nodes={commentsToFlatNodes(this.state.comments)}
278             noIndent
279             locked={this.state.post.locked}
280             moderators={this.state.moderators}
281             admins={this.state.admins}
282             postCreatorId={this.state.post.creator_id}
283             showContext
284           />
285         </div>
286       </div>
287     );
288   }
289
290   sidebar() {
291     return (
292       <div class="mb-3">
293         <Sidebar
294           community={this.state.community}
295           moderators={this.state.moderators}
296           admins={this.state.admins}
297           online={this.state.online}
298         />
299       </div>
300     );
301   }
302
303   handleCommentSortChange(i: Post, event: any) {
304     i.state.commentSort = Number(event.target.value);
305     i.setState(i.state);
306   }
307
308   buildCommentsTree(): Array<CommentNodeI> {
309     let map = new Map<number, CommentNodeI>();
310     for (let comment of this.state.comments) {
311       let node: CommentNodeI = {
312         comment: comment,
313         children: [],
314       };
315       map.set(comment.id, { ...node });
316     }
317     let tree: Array<CommentNodeI> = [];
318     for (let comment of this.state.comments) {
319       let child = map.get(comment.id);
320       if (comment.parent_id) {
321         let parent_ = map.get(comment.parent_id);
322         parent_.children.push(child);
323       } else {
324         tree.push(child);
325       }
326
327       this.setDepth(child);
328     }
329
330     return tree;
331   }
332
333   setDepth(node: CommentNodeI, i: number = 0): void {
334     for (let child of node.children) {
335       child.comment.depth = i;
336       this.setDepth(child, i + 1);
337     }
338   }
339
340   commentsTree() {
341     let nodes = this.buildCommentsTree();
342     return (
343       <div>
344         <CommentNodes
345           nodes={nodes}
346           locked={this.state.post.locked}
347           moderators={this.state.moderators}
348           admins={this.state.admins}
349           postCreatorId={this.state.post.creator_id}
350           sort={this.state.commentSort}
351         />
352       </div>
353     );
354   }
355
356   parseMessage(msg: WebSocketJsonResponse) {
357     console.log(msg);
358     let res = wsJsonToRes(msg);
359     if (msg.error) {
360       toast(i18n.t(msg.error), 'danger');
361       return;
362     } else if (msg.reconnect) {
363       WebSocketService.Instance.getPost({
364         id: Number(this.props.match.params.id),
365       });
366     } else if (res.op == UserOperation.GetPost) {
367       let data = res.data as GetPostResponse;
368       this.state.post = data.post;
369       this.state.comments = data.comments;
370       this.state.community = data.community;
371       this.state.moderators = data.moderators;
372       this.state.admins = data.admins;
373       this.state.online = data.online;
374       this.state.loading = false;
375       document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
376
377       // Get cross-posts
378       if (this.state.post.url) {
379         let form: SearchForm = {
380           q: this.state.post.url,
381           type_: SearchType[SearchType.Url],
382           sort: SortType[SortType.TopAll],
383           page: 1,
384           limit: 6,
385         };
386         WebSocketService.Instance.search(form);
387       }
388
389       this.setState(this.state);
390       setupTippy();
391     } else if (res.op == UserOperation.CreateComment) {
392       let data = res.data as CommentResponse;
393
394       // Necessary since it might be a user reply
395       if (data.recipient_ids.length == 0) {
396         this.state.comments.unshift(data.comment);
397         this.setState(this.state);
398       }
399     } else if (res.op == UserOperation.EditComment) {
400       let data = res.data as CommentResponse;
401       editCommentRes(data, this.state.comments);
402       this.setState(this.state);
403     } else if (res.op == UserOperation.SaveComment) {
404       let data = res.data as CommentResponse;
405       saveCommentRes(data, this.state.comments);
406       this.setState(this.state);
407       setupTippy();
408     } else if (res.op == UserOperation.CreateCommentLike) {
409       let data = res.data as CommentResponse;
410       createCommentLikeRes(data, this.state.comments);
411       this.setState(this.state);
412     } else if (res.op == UserOperation.CreatePostLike) {
413       let data = res.data as PostResponse;
414       createPostLikeRes(data, this.state.post);
415       this.setState(this.state);
416     } else if (res.op == UserOperation.EditPost) {
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.SavePost) {
422       let data = res.data as PostResponse;
423       this.state.post = data.post;
424       this.setState(this.state);
425       setupTippy();
426     } else if (res.op == UserOperation.EditCommunity) {
427       let data = res.data as CommunityResponse;
428       this.state.community = data.community;
429       this.state.post.community_id = data.community.id;
430       this.state.post.community_name = data.community.name;
431       this.setState(this.state);
432     } else if (res.op == UserOperation.FollowCommunity) {
433       let data = res.data as CommunityResponse;
434       this.state.community.subscribed = data.community.subscribed;
435       this.state.community.number_of_subscribers =
436         data.community.number_of_subscribers;
437       this.setState(this.state);
438     } else if (res.op == UserOperation.BanFromCommunity) {
439       let data = res.data as BanFromCommunityResponse;
440       this.state.comments
441         .filter(c => c.creator_id == data.user.id)
442         .forEach(c => (c.banned_from_community = data.banned));
443       if (this.state.post.creator_id == data.user.id) {
444         this.state.post.banned_from_community = data.banned;
445       }
446       this.setState(this.state);
447     } else if (res.op == UserOperation.AddModToCommunity) {
448       let data = res.data as AddModToCommunityResponse;
449       this.state.moderators = data.moderators;
450       this.setState(this.state);
451     } else if (res.op == UserOperation.BanUser) {
452       let data = res.data as BanUserResponse;
453       this.state.comments
454         .filter(c => c.creator_id == data.user.id)
455         .forEach(c => (c.banned = data.banned));
456       if (this.state.post.creator_id == data.user.id) {
457         this.state.post.banned = data.banned;
458       }
459       this.setState(this.state);
460     } else if (res.op == UserOperation.AddAdmin) {
461       let data = res.data as AddAdminResponse;
462       this.state.admins = data.admins;
463       this.setState(this.state);
464     } else if (res.op == UserOperation.Search) {
465       let data = res.data as SearchResponse;
466       this.state.crossPosts = data.posts.filter(
467         p => p.id != Number(this.props.match.params.id)
468       );
469       this.setState(this.state);
470     } else if (res.op == UserOperation.TransferSite) {
471       let data = res.data as GetSiteResponse;
472       this.state.admins = data.admins;
473       this.setState(this.state);
474     } else if (res.op == UserOperation.TransferCommunity) {
475       let data = res.data as GetCommunityResponse;
476       this.state.community = data.community;
477       this.state.moderators = data.moderators;
478       this.state.admins = data.admins;
479       this.setState(this.state);
480     }
481   }
482 }