]> Untitled Git - lemmy.git/blob - ui/src/components/post.tsx
Merge branch 'master' into dev
[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   hotRank,
35   toast,
36   editCommentRes,
37   saveCommentRes,
38   createCommentLikeRes,
39   createPostLikeRes,
40   commentsToFlatNodes,
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     }
161   }
162
163   render() {
164     return (
165       <div class="container">
166         {this.state.loading ? (
167           <h5>
168             <svg class="icon icon-spinner spin">
169               <use xlinkHref="#icon-spinner"></use>
170             </svg>
171           </h5>
172         ) : (
173           <div class="row">
174             <div class="col-12 col-md-8 mb-3">
175               <PostListing
176                 post={this.state.post}
177                 showBody
178                 showCommunity
179                 moderators={this.state.moderators}
180                 admins={this.state.admins}
181               />
182               {this.state.crossPosts.length > 0 && (
183                 <>
184                   <div class="my-1 text-muted small font-weight-bold">
185                     {i18n.t('cross_posts')}
186                   </div>
187                   <PostListings showCommunity posts={this.state.crossPosts} />
188                 </>
189               )}
190               <div className="mb-2" />
191               <CommentForm
192                 postId={this.state.post.id}
193                 disabled={this.state.post.locked}
194               />
195               {this.state.comments.length > 0 && this.sortRadios()}
196               {this.commentsTree()}
197             </div>
198             <div class="col-12 col-sm-12 col-md-4">
199               {this.state.comments.length > 0 && this.newComments()}
200               {this.sidebar()}
201             </div>
202           </div>
203         )}
204       </div>
205     );
206   }
207
208   sortRadios() {
209     return (
210       <div class="btn-group btn-group-toggle mb-3">
211         <label
212           className={`btn btn-sm btn-secondary pointer ${this.state
213             .commentSort === CommentSortType.Hot && 'active'}`}
214         >
215           {i18n.t('hot')}
216           <input
217             type="radio"
218             value={CommentSortType.Hot}
219             checked={this.state.commentSort === CommentSortType.Hot}
220             onChange={linkEvent(this, this.handleCommentSortChange)}
221           />
222         </label>
223         <label
224           className={`btn btn-sm btn-secondary pointer ${this.state
225             .commentSort === CommentSortType.Top && 'active'}`}
226         >
227           {i18n.t('top')}
228           <input
229             type="radio"
230             value={CommentSortType.Top}
231             checked={this.state.commentSort === CommentSortType.Top}
232             onChange={linkEvent(this, this.handleCommentSortChange)}
233           />
234         </label>
235         <label
236           className={`btn btn-sm btn-secondary pointer ${this.state
237             .commentSort === CommentSortType.New && 'active'}`}
238         >
239           {i18n.t('new')}
240           <input
241             type="radio"
242             value={CommentSortType.New}
243             checked={this.state.commentSort === CommentSortType.New}
244             onChange={linkEvent(this, this.handleCommentSortChange)}
245           />
246         </label>
247         <label
248           className={`btn btn-sm btn-secondary pointer ${this.state
249             .commentSort === CommentSortType.Old && 'active'}`}
250         >
251           {i18n.t('old')}
252           <input
253             type="radio"
254             value={CommentSortType.Old}
255             checked={this.state.commentSort === CommentSortType.Old}
256             onChange={linkEvent(this, this.handleCommentSortChange)}
257           />
258         </label>
259       </div>
260     );
261   }
262
263   newComments() {
264     return (
265       <div class="d-none d-md-block new-comments mb-3 card border-secondary">
266         <div class="card-body small">
267           <h6>{i18n.t('recent_comments')}</h6>
268           <CommentNodes
269             nodes={commentsToFlatNodes(this.state.comments)}
270             noIndent
271             locked={this.state.post.locked}
272             moderators={this.state.moderators}
273             admins={this.state.admins}
274             postCreatorId={this.state.post.creator_id}
275           />
276         </div>
277       </div>
278     );
279   }
280
281   sidebar() {
282     return (
283       <div class="mb-3">
284         <Sidebar
285           community={this.state.community}
286           moderators={this.state.moderators}
287           admins={this.state.admins}
288           online={this.state.online}
289         />
290       </div>
291     );
292   }
293
294   handleCommentSortChange(i: Post, event: any) {
295     i.state.commentSort = Number(event.target.value);
296     i.setState(i.state);
297   }
298
299   private buildCommentsTree(): Array<CommentNodeI> {
300     let map = new Map<number, CommentNodeI>();
301     for (let comment of this.state.comments) {
302       let node: CommentNodeI = {
303         comment: comment,
304         children: [],
305       };
306       map.set(comment.id, { ...node });
307     }
308     let tree: Array<CommentNodeI> = [];
309     for (let comment of this.state.comments) {
310       if (comment.parent_id) {
311         map.get(comment.parent_id).children.push(map.get(comment.id));
312       } else {
313         tree.push(map.get(comment.id));
314       }
315     }
316
317     this.sortTree(tree);
318
319     return tree;
320   }
321
322   sortTree(tree: Array<CommentNodeI>) {
323     // First, put removed and deleted comments at the bottom, then do your other sorts
324     if (this.state.commentSort == CommentSortType.Top) {
325       tree.sort(
326         (a, b) =>
327           +a.comment.removed - +b.comment.removed ||
328           +a.comment.deleted - +b.comment.deleted ||
329           b.comment.score - a.comment.score
330       );
331     } else if (this.state.commentSort == CommentSortType.New) {
332       tree.sort(
333         (a, b) =>
334           +a.comment.removed - +b.comment.removed ||
335           +a.comment.deleted - +b.comment.deleted ||
336           b.comment.published.localeCompare(a.comment.published)
337       );
338     } else if (this.state.commentSort == CommentSortType.Old) {
339       tree.sort(
340         (a, b) =>
341           +a.comment.removed - +b.comment.removed ||
342           +a.comment.deleted - +b.comment.deleted ||
343           a.comment.published.localeCompare(b.comment.published)
344       );
345     } else if (this.state.commentSort == CommentSortType.Hot) {
346       tree.sort(
347         (a, b) =>
348           +a.comment.removed - +b.comment.removed ||
349           +a.comment.deleted - +b.comment.deleted ||
350           hotRank(b.comment) - hotRank(a.comment)
351       );
352     }
353
354     for (let node of tree) {
355       this.sortTree(node.children);
356     }
357   }
358
359   commentsTree() {
360     let nodes = this.buildCommentsTree();
361     return (
362       <div>
363         <CommentNodes
364           nodes={nodes}
365           locked={this.state.post.locked}
366           moderators={this.state.moderators}
367           admins={this.state.admins}
368           postCreatorId={this.state.post.creator_id}
369         />
370       </div>
371     );
372   }
373
374   parseMessage(msg: WebSocketJsonResponse) {
375     console.log(msg);
376     let res = wsJsonToRes(msg);
377     if (msg.error) {
378       toast(i18n.t(msg.error), 'danger');
379       return;
380     } else if (msg.reconnect) {
381       WebSocketService.Instance.getPost({
382         id: Number(this.props.match.params.id),
383       });
384     } else if (res.op == UserOperation.GetPost) {
385       let data = res.data as GetPostResponse;
386       this.state.post = data.post;
387       this.state.comments = data.comments;
388       this.state.community = data.community;
389       this.state.moderators = data.moderators;
390       this.state.admins = data.admins;
391       this.state.online = data.online;
392       this.state.loading = false;
393       document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
394
395       // Get cross-posts
396       if (this.state.post.url) {
397         let form: SearchForm = {
398           q: this.state.post.url,
399           type_: SearchType[SearchType.Url],
400           sort: SortType[SortType.TopAll],
401           page: 1,
402           limit: 6,
403         };
404         WebSocketService.Instance.search(form);
405       }
406
407       this.setState(this.state);
408     } else if (res.op == UserOperation.CreateComment) {
409       let data = res.data as CommentResponse;
410
411       // Necessary since it might be a user reply
412       if (data.recipient_ids.length == 0) {
413         this.state.comments.unshift(data.comment);
414         this.setState(this.state);
415       }
416     } else if (res.op == UserOperation.EditComment) {
417       let data = res.data as CommentResponse;
418       editCommentRes(data, this.state.comments);
419       this.setState(this.state);
420     } else if (res.op == UserOperation.SaveComment) {
421       let data = res.data as CommentResponse;
422       saveCommentRes(data, this.state.comments);
423       this.setState(this.state);
424     } else if (res.op == UserOperation.CreateCommentLike) {
425       let data = res.data as CommentResponse;
426       createCommentLikeRes(data, this.state.comments);
427       this.setState(this.state);
428     } else if (res.op == UserOperation.CreatePostLike) {
429       let data = res.data as PostResponse;
430       createPostLikeRes(data, this.state.post);
431       this.setState(this.state);
432     } else if (res.op == UserOperation.EditPost) {
433       let data = res.data as PostResponse;
434       this.state.post = data.post;
435       this.setState(this.state);
436     } else if (res.op == UserOperation.SavePost) {
437       let data = res.data as PostResponse;
438       this.state.post = data.post;
439       this.setState(this.state);
440     } else if (res.op == UserOperation.EditCommunity) {
441       let data = res.data as CommunityResponse;
442       this.state.community = data.community;
443       this.state.post.community_id = data.community.id;
444       this.state.post.community_name = data.community.name;
445       this.setState(this.state);
446     } else if (res.op == UserOperation.FollowCommunity) {
447       let data = res.data as CommunityResponse;
448       this.state.community.subscribed = data.community.subscribed;
449       this.state.community.number_of_subscribers =
450         data.community.number_of_subscribers;
451       this.setState(this.state);
452     } else if (res.op == UserOperation.BanFromCommunity) {
453       let data = res.data as BanFromCommunityResponse;
454       this.state.comments
455         .filter(c => c.creator_id == data.user.id)
456         .forEach(c => (c.banned_from_community = data.banned));
457       if (this.state.post.creator_id == data.user.id) {
458         this.state.post.banned_from_community = data.banned;
459       }
460       this.setState(this.state);
461     } else if (res.op == UserOperation.AddModToCommunity) {
462       let data = res.data as AddModToCommunityResponse;
463       this.state.moderators = data.moderators;
464       this.setState(this.state);
465     } else if (res.op == UserOperation.BanUser) {
466       let data = res.data as BanUserResponse;
467       this.state.comments
468         .filter(c => c.creator_id == data.user.id)
469         .forEach(c => (c.banned = data.banned));
470       if (this.state.post.creator_id == data.user.id) {
471         this.state.post.banned = data.banned;
472       }
473       this.setState(this.state);
474     } else if (res.op == UserOperation.AddAdmin) {
475       let data = res.data as AddAdminResponse;
476       this.state.admins = data.admins;
477       this.setState(this.state);
478     } else if (res.op == UserOperation.Search) {
479       let data = res.data as SearchResponse;
480       this.state.crossPosts = data.posts.filter(
481         p => p.id != this.state.post.id
482       );
483       this.setState(this.state);
484     } else if (res.op == UserOperation.TransferSite) {
485       let data = res.data as GetSiteResponse;
486       this.state.admins = data.admins;
487       this.setState(this.state);
488     } else if (res.op == UserOperation.TransferCommunity) {
489       let data = res.data as GetCommunityResponse;
490       this.state.community = data.community;
491       this.state.moderators = data.moderators;
492       this.state.admins = data.admins;
493       this.setState(this.state);
494     }
495   }
496 }