]> Untitled Git - lemmy.git/blob - ui/src/components/post.tsx
Initial post-listing community non-local.
[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 ${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             showContext
280           />
281         </div>
282       </div>
283     );
284   }
285
286   sidebar() {
287     return (
288       <div class="mb-3">
289         <Sidebar
290           community={this.state.community}
291           moderators={this.state.moderators}
292           admins={this.state.admins}
293           online={this.state.online}
294         />
295       </div>
296     );
297   }
298
299   handleCommentSortChange(i: Post, event: any) {
300     i.state.commentSort = Number(event.target.value);
301     i.setState(i.state);
302   }
303
304   buildCommentsTree(): Array<CommentNodeI> {
305     let map = new Map<number, CommentNodeI>();
306     for (let comment of this.state.comments) {
307       let node: CommentNodeI = {
308         comment: comment,
309         children: [],
310       };
311       map.set(comment.id, { ...node });
312     }
313     let tree: Array<CommentNodeI> = [];
314     for (let comment of this.state.comments) {
315       let child = map.get(comment.id);
316       if (comment.parent_id) {
317         let parent_ = map.get(comment.parent_id);
318         parent_.children.push(child);
319       } else {
320         tree.push(child);
321       }
322
323       this.setDepth(child);
324     }
325
326     return tree;
327   }
328
329   setDepth(node: CommentNodeI, i: number = 0): void {
330     for (let child of node.children) {
331       child.comment.depth = i;
332       this.setDepth(child, i + 1);
333     }
334   }
335
336   commentsTree() {
337     let nodes = this.buildCommentsTree();
338     return (
339       <div>
340         <CommentNodes
341           nodes={nodes}
342           locked={this.state.post.locked}
343           moderators={this.state.moderators}
344           admins={this.state.admins}
345           postCreatorId={this.state.post.creator_id}
346           sort={this.state.commentSort}
347         />
348       </div>
349     );
350   }
351
352   parseMessage(msg: WebSocketJsonResponse) {
353     console.log(msg);
354     let res = wsJsonToRes(msg);
355     if (msg.error) {
356       toast(i18n.t(msg.error), 'danger');
357       return;
358     } else if (msg.reconnect) {
359       WebSocketService.Instance.getPost({
360         id: Number(this.props.match.params.id),
361       });
362     } else if (res.op == UserOperation.GetPost) {
363       let data = res.data as GetPostResponse;
364       this.state.post = data.post;
365       this.state.comments = data.comments;
366       this.state.community = data.community;
367       this.state.moderators = data.moderators;
368       this.state.admins = data.admins;
369       this.state.online = data.online;
370       this.state.loading = false;
371       document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
372
373       // Get cross-posts
374       if (this.state.post.url) {
375         let form: SearchForm = {
376           q: this.state.post.url,
377           type_: SearchType[SearchType.Url],
378           sort: SortType[SortType.TopAll],
379           page: 1,
380           limit: 6,
381         };
382         WebSocketService.Instance.search(form);
383       }
384
385       this.setState(this.state);
386       setupTippy();
387     } else if (res.op == UserOperation.CreateComment) {
388       let data = res.data as CommentResponse;
389
390       // Necessary since it might be a user reply
391       if (data.recipient_ids.length == 0) {
392         this.state.comments.unshift(data.comment);
393         this.setState(this.state);
394       }
395     } else if (res.op == UserOperation.EditComment) {
396       let data = res.data as CommentResponse;
397       editCommentRes(data, this.state.comments);
398       this.setState(this.state);
399     } else if (res.op == UserOperation.SaveComment) {
400       let data = res.data as CommentResponse;
401       saveCommentRes(data, this.state.comments);
402       this.setState(this.state);
403       setupTippy();
404     } else if (res.op == UserOperation.CreateCommentLike) {
405       let data = res.data as CommentResponse;
406       createCommentLikeRes(data, this.state.comments);
407       this.setState(this.state);
408     } else if (res.op == UserOperation.CreatePostLike) {
409       let data = res.data as PostResponse;
410       createPostLikeRes(data, this.state.post);
411       this.setState(this.state);
412     } else if (res.op == UserOperation.EditPost) {
413       let data = res.data as PostResponse;
414       this.state.post = data.post;
415       this.setState(this.state);
416       setupTippy();
417     } else if (res.op == UserOperation.SavePost) {
418       let data = res.data as PostResponse;
419       this.state.post = data.post;
420       this.setState(this.state);
421       setupTippy();
422     } else if (res.op == UserOperation.EditCommunity) {
423       let data = res.data as CommunityResponse;
424       this.state.community = data.community;
425       this.state.post.community_id = data.community.id;
426       this.state.post.community_name = data.community.name;
427       this.setState(this.state);
428     } else if (res.op == UserOperation.FollowCommunity) {
429       let data = res.data as CommunityResponse;
430       this.state.community.subscribed = data.community.subscribed;
431       this.state.community.number_of_subscribers =
432         data.community.number_of_subscribers;
433       this.setState(this.state);
434     } else if (res.op == UserOperation.BanFromCommunity) {
435       let data = res.data as BanFromCommunityResponse;
436       this.state.comments
437         .filter(c => c.creator_id == data.user.id)
438         .forEach(c => (c.banned_from_community = data.banned));
439       if (this.state.post.creator_id == data.user.id) {
440         this.state.post.banned_from_community = data.banned;
441       }
442       this.setState(this.state);
443     } else if (res.op == UserOperation.AddModToCommunity) {
444       let data = res.data as AddModToCommunityResponse;
445       this.state.moderators = data.moderators;
446       this.setState(this.state);
447     } else if (res.op == UserOperation.BanUser) {
448       let data = res.data as BanUserResponse;
449       this.state.comments
450         .filter(c => c.creator_id == data.user.id)
451         .forEach(c => (c.banned = data.banned));
452       if (this.state.post.creator_id == data.user.id) {
453         this.state.post.banned = data.banned;
454       }
455       this.setState(this.state);
456     } else if (res.op == UserOperation.AddAdmin) {
457       let data = res.data as AddAdminResponse;
458       this.state.admins = data.admins;
459       this.setState(this.state);
460     } else if (res.op == UserOperation.Search) {
461       let data = res.data as SearchResponse;
462       this.state.crossPosts = data.posts.filter(
463         p => p.id != this.state.post.id
464       );
465       this.setState(this.state);
466     } else if (res.op == UserOperation.TransferSite) {
467       let data = res.data as GetSiteResponse;
468       this.state.admins = data.admins;
469       this.setState(this.state);
470     } else if (res.op == UserOperation.TransferCommunity) {
471       let data = res.data as GetCommunityResponse;
472       this.state.community = data.community;
473       this.state.moderators = data.moderators;
474       this.state.admins = data.admins;
475       this.setState(this.state);
476     }
477   }
478 }