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