1 import { Component, linkEvent } from 'inferno';
2 import { Subscription } from "rxjs";
3 import { retryWhen, delay, take } from 'rxjs/operators';
4 import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, BanUserResponse, AddModToCommunityResponse, AddAdminResponse, UserView } from '../interfaces';
5 import { WebSocketService, UserService } from '../services';
6 import { msgOp, hotRank } from '../utils';
7 import { PostListing } from './post-listing';
8 import { Sidebar } from './sidebar';
9 import { CommentForm } from './comment-form';
10 import { CommentNodes } from './comment-nodes';
11 import * as autosize from 'autosize';
15 comments: Array<Comment>;
16 commentSort: CommentSortType;
18 moderators: Array<CommunityUser>;
19 admins: Array<UserView>;
21 scrolled_comment_id?: number;
25 export class Post extends Component<any, PostState> {
27 private subscription: Subscription;
28 private emptyState: PostState = {
31 commentSort: CommentSortType.Hot,
39 constructor(props: any, context: any) {
40 super(props, context);
42 this.state = this.emptyState;
44 let postId = Number(this.props.match.params.id);
45 if (this.props.match.params.comment_id) {
46 this.state.scrolled_comment_id = this.props.match.params.comment_id;
49 this.subscription = WebSocketService.Instance.subject
50 .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
52 (msg) => this.parseMessage(msg),
53 (err) => console.error(err),
54 () => console.log('complete')
57 WebSocketService.Instance.getPost(postId);
60 componentWillUnmount() {
61 this.subscription.unsubscribe();
65 autosize(document.querySelectorAll('textarea'));
68 componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
69 if (this.state.scrolled_comment_id && !this.state.scrolled && lastState.comments.length > 0) {
70 var elmnt = document.getElementById(`comment-${this.state.scrolled_comment_id}`);
71 elmnt.scrollIntoView();
72 elmnt.classList.add("mark-two");
73 this.state.scrolled = true;
74 this.markScrolledAsRead(this.state.scrolled_comment_id);
78 markScrolledAsRead(commentId: number) {
79 let found = this.state.comments.find(c => c.id == commentId);
80 let parent = this.state.comments.find(c => found.parent_id == c.id);
81 let parent_user_id = parent ? parent.creator_id : this.state.post.creator_id;
83 if (UserService.Instance.user && UserService.Instance.user.id == parent_user_id) {
85 let form: CommentFormI = {
86 content: found.content,
88 creator_id: found.creator_id,
89 post_id: found.post_id,
90 parent_id: found.parent_id,
94 WebSocketService.Instance.editComment(form);
100 <div class="container">
101 {this.state.loading ?
102 <h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> :
104 <div class="col-12 col-md-8 col-lg-6 mb-3">
106 post={this.state.post}
110 moderators={this.state.moderators}
111 admins={this.state.admins}
113 <div className="mb-2" />
114 <CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
116 {this.commentsTree()}
118 <div class="col-12 col-md-4 col-lg-3 mb-3 d-none d-md-block px-0">
119 {this.state.comments.length > 0 && this.newComments()}
121 <div class="col-12 col-sm-12 col-lg-3">
132 <div class="btn-group btn-group-toggle mb-3">
133 <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>Hot
134 <input type="radio" value={CommentSortType.Hot}
135 checked={this.state.commentSort === CommentSortType.Hot}
136 onChange={linkEvent(this, this.handleCommentSortChange)} />
138 <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Top && 'active'}`}>Top
139 <input type="radio" value={CommentSortType.Top}
140 checked={this.state.commentSort === CommentSortType.Top}
141 onChange={linkEvent(this, this.handleCommentSortChange)} />
143 <label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.New && 'active'}`}>New
144 <input type="radio" value={CommentSortType.New}
145 checked={this.state.commentSort === CommentSortType.New}
146 onChange={linkEvent(this, this.handleCommentSortChange)} />
154 <div class="container-fluid sticky-top new-comments">
156 <CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
157 {this.state.comments.map(comment =>
159 nodes={[{comment: comment}]}
161 locked={this.state.post.locked}
162 moderators={this.state.moderators}
163 admins={this.state.admins}
174 community={this.state.community}
175 moderators={this.state.moderators}
176 admins={this.state.admins}
182 handleCommentSortChange(i: Post, event: any) {
183 i.state.commentSort = Number(event.target.value);
187 private buildCommentsTree(): Array<CommentNodeI> {
188 let map = new Map<number, CommentNodeI>();
189 for (let comment of this.state.comments) {
190 let node: CommentNodeI = {
194 map.set(comment.id, { ...node });
196 let tree: Array<CommentNodeI> = [];
197 for (let comment of this.state.comments) {
198 if( comment.parent_id ) {
199 map.get(comment.parent_id).children.push(map.get(comment.id));
202 tree.push(map.get(comment.id));
211 sortTree(tree: Array<CommentNodeI>) {
213 if (this.state.commentSort == CommentSortType.Top) {
214 tree.sort((a, b) => b.comment.score - a.comment.score);
215 } else if (this.state.commentSort == CommentSortType.New) {
216 tree.sort((a, b) => b.comment.published.localeCompare(a.comment.published));
217 } else if (this.state.commentSort == CommentSortType.Hot) {
218 tree.sort((a, b) => hotRank(b.comment) - hotRank(a.comment));
221 for (let node of tree) {
222 this.sortTree(node.children);
228 let nodes = this.buildCommentsTree();
233 locked={this.state.post.locked}
234 moderators={this.state.moderators}
235 admins={this.state.admins}
241 parseMessage(msg: any) {
243 let op: UserOperation = msgOp(msg);
247 } else if (op == UserOperation.GetPost) {
248 let res: GetPostResponse = msg;
249 this.state.post = res.post;
250 this.state.post = res.post;
251 this.state.comments = res.comments;
252 this.state.community = res.community;
253 this.state.moderators = res.moderators;
254 this.state.admins = res.admins;
255 this.state.loading = false;
256 document.title = `${this.state.post.name} - Lemmy`;
257 this.setState(this.state);
258 } else if (op == UserOperation.CreateComment) {
259 let res: CommentResponse = msg;
260 this.state.comments.unshift(res.comment);
261 this.setState(this.state);
262 } else if (op == UserOperation.EditComment) {
263 let res: CommentResponse = msg;
264 let found = this.state.comments.find(c => c.id == res.comment.id);
265 found.content = res.comment.content;
266 found.updated = res.comment.updated;
267 found.removed = res.comment.removed;
268 found.deleted = res.comment.deleted;
269 found.upvotes = res.comment.upvotes;
270 found.downvotes = res.comment.downvotes;
271 found.score = res.comment.score;
272 found.read = res.comment.read;
274 this.setState(this.state);
275 } else if (op == UserOperation.SaveComment) {
276 let res: CommentResponse = msg;
277 let found = this.state.comments.find(c => c.id == res.comment.id);
278 found.saved = res.comment.saved;
279 this.setState(this.state);
280 } else if (op == UserOperation.CreateCommentLike) {
281 let res: CommentResponse = msg;
282 let found: Comment = this.state.comments.find(c => c.id === res.comment.id);
283 found.score = res.comment.score;
284 found.upvotes = res.comment.upvotes;
285 found.downvotes = res.comment.downvotes;
286 if (res.comment.my_vote !== null)
287 found.my_vote = res.comment.my_vote;
288 this.setState(this.state);
289 } else if (op == UserOperation.CreatePostLike) {
290 let res: CreatePostLikeResponse = msg;
291 this.state.post.my_vote = res.post.my_vote;
292 this.state.post.score = res.post.score;
293 this.state.post.upvotes = res.post.upvotes;
294 this.state.post.downvotes = res.post.downvotes;
295 this.setState(this.state);
296 } else if (op == UserOperation.EditPost) {
297 let res: PostResponse = msg;
298 this.state.post = res.post;
299 this.setState(this.state);
300 } else if (op == UserOperation.SavePost) {
301 let res: PostResponse = msg;
302 this.state.post = res.post;
303 this.setState(this.state);
304 } else if (op == UserOperation.EditCommunity) {
305 let res: CommunityResponse = msg;
306 this.state.community = res.community;
307 this.state.post.community_id = res.community.id;
308 this.state.post.community_name = res.community.name;
309 this.setState(this.state);
310 } else if (op == UserOperation.FollowCommunity) {
311 let res: CommunityResponse = msg;
312 this.state.community.subscribed = res.community.subscribed;
313 this.state.community.number_of_subscribers = res.community.number_of_subscribers;
314 this.setState(this.state);
315 } else if (op == UserOperation.BanFromCommunity) {
316 let res: BanFromCommunityResponse = msg;
317 this.state.comments.filter(c => c.creator_id == res.user.id)
318 .forEach(c => c.banned_from_community = res.banned);
319 this.setState(this.state);
320 } else if (op == UserOperation.AddModToCommunity) {
321 let res: AddModToCommunityResponse = msg;
322 this.state.moderators = res.moderators;
323 this.setState(this.state);
324 } else if (op == UserOperation.BanUser) {
325 let res: BanUserResponse = msg;
326 this.state.comments.filter(c => c.creator_id == res.user.id)
327 .forEach(c => c.banned = res.banned);
328 this.setState(this.state);
329 } else if (op == UserOperation.AddAdmin) {
330 let res: AddAdminResponse = msg;
331 this.state.admins = res.admins;
332 this.setState(this.state);