1 import { Component, linkEvent } from 'inferno';
2 import { Subscription } from 'rxjs';
3 import { retryWhen, delay, take } from 'rxjs/operators';
11 CommentForm as CommentFormI,
16 CommentNode as CommentNodeI,
17 BanFromCommunityResponse,
19 AddModToCommunityResponse,
29 WebSocketJsonResponse,
30 } from '../interfaces';
31 import { WebSocketService, UserService } from '../services';
32 import { wsJsonToRes, hotRank, toast } from '../utils';
33 import { PostListing } from './post-listing';
34 import { PostListings } from './post-listings';
35 import { Sidebar } from './sidebar';
36 import { CommentForm } from './comment-form';
37 import { CommentNodes } from './comment-nodes';
38 import autosize from 'autosize';
39 import { i18n } from '../i18next';
40 import { T } from 'inferno-i18next';
44 comments: Array<Comment>;
45 commentSort: CommentSortType;
47 moderators: Array<CommunityUser>;
48 admins: Array<UserView>;
51 scrolled_comment_id?: number;
53 crossPosts: Array<PostI>;
56 export class Post extends Component<any, PostState> {
57 private subscription: Subscription;
58 private emptyState: PostState = {
61 commentSort: CommentSortType.Hot,
71 constructor(props: any, context: any) {
72 super(props, context);
74 this.state = this.emptyState;
76 let postId = Number(this.props.match.params.id);
77 if (this.props.match.params.comment_id) {
78 this.state.scrolled_comment_id = this.props.match.params.comment_id;
81 this.subscription = WebSocketService.Instance.subject
82 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
84 msg => this.parseMessage(msg),
85 err => console.error(err),
86 () => console.log('complete')
89 let form: GetPostForm = {
92 WebSocketService.Instance.getPost(form);
95 componentWillUnmount() {
96 this.subscription.unsubscribe();
100 autosize(document.querySelectorAll('textarea'));
103 componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
105 this.state.scrolled_comment_id &&
106 !this.state.scrolled &&
107 lastState.comments.length > 0
109 var elmnt = document.getElementById(
110 `comment-${this.state.scrolled_comment_id}`
112 elmnt.scrollIntoView();
113 elmnt.classList.add('mark');
114 this.state.scrolled = true;
115 this.markScrolledAsRead(this.state.scrolled_comment_id);
118 // Necessary if you are on a post and you click another post (same route)
119 if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
120 // Couldnt get a refresh working. This does for now.
123 // let currentId = this.props.match.params.id;
124 // WebSocketService.Instance.getPost(currentId);
125 // this.context.router.history.push('/sponsors');
126 // this.context.refresh();
127 // this.context.router.history.push(_lastProps.location.pathname);
131 markScrolledAsRead(commentId: number) {
132 let found = this.state.comments.find(c => c.id == commentId);
133 let parent = this.state.comments.find(c => found.parent_id == c.id);
134 let parent_user_id = parent
136 : this.state.post.creator_id;
139 UserService.Instance.user &&
140 UserService.Instance.user.id == parent_user_id
142 let form: CommentFormI = {
143 content: found.content,
145 creator_id: found.creator_id,
146 post_id: found.post_id,
147 parent_id: found.parent_id,
151 WebSocketService.Instance.editComment(form);
157 <div class="container">
158 {this.state.loading ? (
160 <svg class="icon icon-spinner spin">
161 <use xlinkHref="#icon-spinner"></use>
166 <div class="col-12 col-md-8 mb-3">
168 post={this.state.post}
171 moderators={this.state.moderators}
172 admins={this.state.admins}
174 {this.state.crossPosts.length > 0 && (
176 <div class="my-1 text-muted small font-weight-bold">
177 <T i18nKey="cross_posts">#</T>
179 <PostListings showCommunity posts={this.state.crossPosts} />
182 <div className="mb-2" />
184 postId={this.state.post.id}
185 disabled={this.state.post.locked}
187 {this.state.comments.length > 0 && this.sortRadios()}
188 {this.commentsTree()}
190 <div class="col-12 col-sm-12 col-md-4">
191 {this.state.comments.length > 0 && this.newComments()}
202 <div class="btn-group btn-group-toggle mb-3">
204 className={`btn btn-sm btn-secondary pointer ${this.state
205 .commentSort === CommentSortType.Hot && 'active'}`}
210 value={CommentSortType.Hot}
211 checked={this.state.commentSort === CommentSortType.Hot}
212 onChange={linkEvent(this, this.handleCommentSortChange)}
216 className={`btn btn-sm btn-secondary pointer ${this.state
217 .commentSort === CommentSortType.Top && 'active'}`}
222 value={CommentSortType.Top}
223 checked={this.state.commentSort === CommentSortType.Top}
224 onChange={linkEvent(this, this.handleCommentSortChange)}
228 className={`btn btn-sm btn-secondary pointer ${this.state
229 .commentSort === CommentSortType.New && 'active'}`}
234 value={CommentSortType.New}
235 checked={this.state.commentSort === CommentSortType.New}
236 onChange={linkEvent(this, this.handleCommentSortChange)}
240 className={`btn btn-sm btn-secondary pointer ${this.state
241 .commentSort === CommentSortType.Old && 'active'}`}
246 value={CommentSortType.Old}
247 checked={this.state.commentSort === CommentSortType.Old}
248 onChange={linkEvent(this, this.handleCommentSortChange)}
257 <div class="d-none d-md-block new-comments mb-3 card border-secondary">
258 <div class="card-body small">
260 <T i18nKey="recent_comments">#</T>
262 {this.state.comments.map(comment => (
264 nodes={[{ comment: comment }]}
266 locked={this.state.post.locked}
267 moderators={this.state.moderators}
268 admins={this.state.admins}
269 postCreatorId={this.state.post.creator_id}
281 community={this.state.community}
282 moderators={this.state.moderators}
283 admins={this.state.admins}
284 online={this.state.online}
290 handleCommentSortChange(i: Post, event: any) {
291 i.state.commentSort = Number(event.target.value);
295 private buildCommentsTree(): Array<CommentNodeI> {
296 let map = new Map<number, CommentNodeI>();
297 for (let comment of this.state.comments) {
298 let node: CommentNodeI = {
302 map.set(comment.id, { ...node });
304 let tree: Array<CommentNodeI> = [];
305 for (let comment of this.state.comments) {
306 if (comment.parent_id) {
307 map.get(comment.parent_id).children.push(map.get(comment.id));
309 tree.push(map.get(comment.id));
318 sortTree(tree: Array<CommentNodeI>) {
319 // First, put removed and deleted comments at the bottom, then do your other sorts
320 if (this.state.commentSort == CommentSortType.Top) {
323 +a.comment.removed - +b.comment.removed ||
324 +a.comment.deleted - +b.comment.deleted ||
325 b.comment.score - a.comment.score
327 } else if (this.state.commentSort == CommentSortType.New) {
330 +a.comment.removed - +b.comment.removed ||
331 +a.comment.deleted - +b.comment.deleted ||
332 b.comment.published.localeCompare(a.comment.published)
334 } else if (this.state.commentSort == CommentSortType.Old) {
337 +a.comment.removed - +b.comment.removed ||
338 +a.comment.deleted - +b.comment.deleted ||
339 a.comment.published.localeCompare(b.comment.published)
341 } else if (this.state.commentSort == CommentSortType.Hot) {
344 +a.comment.removed - +b.comment.removed ||
345 +a.comment.deleted - +b.comment.deleted ||
346 hotRank(b.comment) - hotRank(a.comment)
350 for (let node of tree) {
351 this.sortTree(node.children);
356 let nodes = this.buildCommentsTree();
361 locked={this.state.post.locked}
362 moderators={this.state.moderators}
363 admins={this.state.admins}
364 postCreatorId={this.state.post.creator_id}
370 parseMessage(msg: WebSocketJsonResponse) {
372 let res = wsJsonToRes(msg);
374 toast(i18n.t(msg.error), 'danger');
376 } else if (res.op == UserOperation.GetPost) {
377 let data = res.data as GetPostResponse;
378 this.state.post = data.post;
379 this.state.comments = data.comments;
380 this.state.community = data.community;
381 this.state.moderators = data.moderators;
382 this.state.admins = data.admins;
383 this.state.online = data.online;
384 this.state.loading = false;
385 document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
388 if (this.state.post.url) {
389 let form: SearchForm = {
390 q: this.state.post.url,
391 type_: SearchType[SearchType.Url],
392 sort: SortType[SortType.TopAll],
396 WebSocketService.Instance.search(form);
399 this.setState(this.state);
400 } else if (res.op == UserOperation.CreateComment) {
401 let data = res.data as CommentResponse;
403 // Necessary since it might be a user reply
404 if (data.recipient_ids.length == 0) {
405 this.state.comments.unshift(data.comment);
406 this.setState(this.state);
408 } else if (res.op == UserOperation.EditComment) {
409 let data = res.data as CommentResponse;
410 let found = this.state.comments.find(c => c.id == data.comment.id);
411 found.content = data.comment.content;
412 found.updated = data.comment.updated;
413 found.removed = data.comment.removed;
414 found.deleted = data.comment.deleted;
415 found.upvotes = data.comment.upvotes;
416 found.downvotes = data.comment.downvotes;
417 found.score = data.comment.score;
418 found.read = data.comment.read;
420 this.setState(this.state);
421 } else if (res.op == UserOperation.SaveComment) {
422 let data = res.data as CommentResponse;
423 let found = this.state.comments.find(c => c.id == data.comment.id);
424 found.saved = data.comment.saved;
425 this.setState(this.state);
426 } else if (res.op == UserOperation.CreateCommentLike) {
427 let data = res.data as CommentResponse;
428 let found: Comment = this.state.comments.find(
429 c => c.id === data.comment.id
431 found.score = data.comment.score;
432 found.upvotes = data.comment.upvotes;
433 found.downvotes = data.comment.downvotes;
434 if (data.comment.my_vote !== null) {
435 found.my_vote = data.comment.my_vote;
436 found.upvoteLoading = false;
437 found.downvoteLoading = false;
439 this.setState(this.state);
440 } else if (res.op == UserOperation.CreatePostLike) {
441 let data = res.data as PostResponse;
442 this.state.post.score = data.post.score;
443 this.state.post.upvotes = data.post.upvotes;
444 this.state.post.downvotes = data.post.downvotes;
445 if (data.post.my_vote !== null) {
446 this.state.post.my_vote = data.post.my_vote;
447 this.state.post.upvoteLoading = false;
448 this.state.post.downvoteLoading = false;
451 this.setState(this.state);
452 } else if (res.op == UserOperation.EditPost) {
453 let data = res.data as PostResponse;
454 this.state.post = data.post;
455 this.setState(this.state);
456 } else if (res.op == UserOperation.SavePost) {
457 let data = res.data as PostResponse;
458 this.state.post = data.post;
459 this.setState(this.state);
460 } else if (res.op == UserOperation.EditCommunity) {
461 let data = res.data as CommunityResponse;
462 this.state.community = data.community;
463 this.state.post.community_id = data.community.id;
464 this.state.post.community_name = data.community.name;
465 this.setState(this.state);
466 } else if (res.op == UserOperation.FollowCommunity) {
467 let data = res.data as CommunityResponse;
468 this.state.community.subscribed = data.community.subscribed;
469 this.state.community.number_of_subscribers =
470 data.community.number_of_subscribers;
471 this.setState(this.state);
472 } else if (res.op == UserOperation.BanFromCommunity) {
473 let data = res.data as BanFromCommunityResponse;
475 .filter(c => c.creator_id == data.user.id)
476 .forEach(c => (c.banned_from_community = data.banned));
477 if (this.state.post.creator_id == data.user.id) {
478 this.state.post.banned_from_community = data.banned;
480 this.setState(this.state);
481 } else if (res.op == UserOperation.AddModToCommunity) {
482 let data = res.data as AddModToCommunityResponse;
483 this.state.moderators = data.moderators;
484 this.setState(this.state);
485 } else if (res.op == UserOperation.BanUser) {
486 let data = res.data as BanUserResponse;
488 .filter(c => c.creator_id == data.user.id)
489 .forEach(c => (c.banned = data.banned));
490 if (this.state.post.creator_id == data.user.id) {
491 this.state.post.banned = data.banned;
493 this.setState(this.state);
494 } else if (res.op == UserOperation.AddAdmin) {
495 let data = res.data as AddAdminResponse;
496 this.state.admins = data.admins;
497 this.setState(this.state);
498 } else if (res.op == UserOperation.Search) {
499 let data = res.data as SearchResponse;
500 this.state.crossPosts = data.posts.filter(
501 p => p.id != this.state.post.id
503 this.setState(this.state);
504 } else if (res.op == UserOperation.TransferSite) {
505 let data = res.data as GetSiteResponse;
507 this.state.admins = data.admins;
508 this.setState(this.state);
509 } else if (res.op == UserOperation.TransferCommunity) {
510 let data = res.data as GetCommunityResponse;
511 this.state.community = data.community;
512 this.state.moderators = data.moderators;
513 this.state.admins = data.admins;
514 this.setState(this.state);