]> Untitled Git - lemmy.git/blob - ui/src/components/post.tsx
Better tippy loading. Fixes #577
[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-3">
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   private 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       if (comment.parent_id) {
315         map.get(comment.parent_id).children.push(map.get(comment.id));
316       } else {
317         tree.push(map.get(comment.id));
318       }
319     }
320
321     return tree;
322   }
323
324   commentsTree() {
325     let nodes = this.buildCommentsTree();
326     return (
327       <div>
328         <CommentNodes
329           nodes={nodes}
330           locked={this.state.post.locked}
331           moderators={this.state.moderators}
332           admins={this.state.admins}
333           postCreatorId={this.state.post.creator_id}
334           sort={this.state.commentSort}
335         />
336       </div>
337     );
338   }
339
340   parseMessage(msg: WebSocketJsonResponse) {
341     console.log(msg);
342     let res = wsJsonToRes(msg);
343     if (msg.error) {
344       toast(i18n.t(msg.error), 'danger');
345       return;
346     } else if (msg.reconnect) {
347       WebSocketService.Instance.getPost({
348         id: Number(this.props.match.params.id),
349       });
350     } else if (res.op == UserOperation.GetPost) {
351       let data = res.data as GetPostResponse;
352       this.state.post = data.post;
353       this.state.comments = data.comments;
354       this.state.community = data.community;
355       this.state.moderators = data.moderators;
356       this.state.admins = data.admins;
357       this.state.online = data.online;
358       this.state.loading = false;
359       document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
360
361       // Get cross-posts
362       if (this.state.post.url) {
363         let form: SearchForm = {
364           q: this.state.post.url,
365           type_: SearchType[SearchType.Url],
366           sort: SortType[SortType.TopAll],
367           page: 1,
368           limit: 6,
369         };
370         WebSocketService.Instance.search(form);
371       }
372
373       this.setState(this.state);
374       setupTippy();
375     } else if (res.op == UserOperation.CreateComment) {
376       let data = res.data as CommentResponse;
377
378       // Necessary since it might be a user reply
379       if (data.recipient_ids.length == 0) {
380         this.state.comments.unshift(data.comment);
381         this.setState(this.state);
382       }
383     } else if (res.op == UserOperation.EditComment) {
384       let data = res.data as CommentResponse;
385       editCommentRes(data, this.state.comments);
386       this.setState(this.state);
387     } else if (res.op == UserOperation.SaveComment) {
388       let data = res.data as CommentResponse;
389       saveCommentRes(data, this.state.comments);
390       this.setState(this.state);
391       setupTippy();
392     } else if (res.op == UserOperation.CreateCommentLike) {
393       let data = res.data as CommentResponse;
394       createCommentLikeRes(data, this.state.comments);
395       this.setState(this.state);
396     } else if (res.op == UserOperation.CreatePostLike) {
397       let data = res.data as PostResponse;
398       createPostLikeRes(data, this.state.post);
399       this.setState(this.state);
400     } else if (res.op == UserOperation.EditPost) {
401       let data = res.data as PostResponse;
402       this.state.post = data.post;
403       this.setState(this.state);
404       setupTippy();
405     } else if (res.op == UserOperation.SavePost) {
406       let data = res.data as PostResponse;
407       this.state.post = data.post;
408       this.setState(this.state);
409       setupTippy();
410     } else if (res.op == UserOperation.EditCommunity) {
411       let data = res.data as CommunityResponse;
412       this.state.community = data.community;
413       this.state.post.community_id = data.community.id;
414       this.state.post.community_name = data.community.name;
415       this.setState(this.state);
416     } else if (res.op == UserOperation.FollowCommunity) {
417       let data = res.data as CommunityResponse;
418       this.state.community.subscribed = data.community.subscribed;
419       this.state.community.number_of_subscribers =
420         data.community.number_of_subscribers;
421       this.setState(this.state);
422     } else if (res.op == UserOperation.BanFromCommunity) {
423       let data = res.data as BanFromCommunityResponse;
424       this.state.comments
425         .filter(c => c.creator_id == data.user.id)
426         .forEach(c => (c.banned_from_community = data.banned));
427       if (this.state.post.creator_id == data.user.id) {
428         this.state.post.banned_from_community = data.banned;
429       }
430       this.setState(this.state);
431     } else if (res.op == UserOperation.AddModToCommunity) {
432       let data = res.data as AddModToCommunityResponse;
433       this.state.moderators = data.moderators;
434       this.setState(this.state);
435     } else if (res.op == UserOperation.BanUser) {
436       let data = res.data as BanUserResponse;
437       this.state.comments
438         .filter(c => c.creator_id == data.user.id)
439         .forEach(c => (c.banned = data.banned));
440       if (this.state.post.creator_id == data.user.id) {
441         this.state.post.banned = data.banned;
442       }
443       this.setState(this.state);
444     } else if (res.op == UserOperation.AddAdmin) {
445       let data = res.data as AddAdminResponse;
446       this.state.admins = data.admins;
447       this.setState(this.state);
448     } else if (res.op == UserOperation.Search) {
449       let data = res.data as SearchResponse;
450       this.state.crossPosts = data.posts.filter(
451         p => p.id != this.state.post.id
452       );
453       this.setState(this.state);
454     } else if (res.op == UserOperation.TransferSite) {
455       let data = res.data as GetSiteResponse;
456       this.state.admins = data.admins;
457       this.setState(this.state);
458     } else if (res.op == UserOperation.TransferCommunity) {
459       let data = res.data as GetCommunityResponse;
460       this.state.community = data.community;
461       this.state.moderators = data.moderators;
462       this.state.admins = data.admins;
463       this.setState(this.state);
464     }
465   }
466 }