1 import { Component, linkEvent } from 'inferno';
2 import { HtmlTags } from './html-tags';
3 import { Subscription } from 'rxjs';
12 BanFromCommunityResponse,
14 AddModToCommunityResponse,
23 ListCategoriesResponse,
25 } from 'lemmy-js-client';
30 CommentNode as CommentNodeI,
31 } from '../interfaces';
32 import { WebSocketService, UserService } from '../services';
44 getCommentIdFromProps,
54 import { PostListing } from './post-listing';
55 import { Sidebar } from './sidebar';
56 import { CommentForm } from './comment-form';
57 import { CommentNodes } from './comment-nodes';
58 import autosize from 'autosize';
59 import { i18n } from '../i18next';
62 postRes: GetPostResponse;
65 commentSort: CommentSortType;
66 commentViewType: CommentViewType;
69 crossPosts: PostView[];
70 siteRes: GetSiteResponse;
71 categories: Category[];
74 export class Post extends Component<any, PostState> {
75 private subscription: Subscription;
76 private isoData = setIsoData(this.context);
77 private emptyState: PostState = {
79 postId: getIdFromProps(this.props),
80 commentId: getCommentIdFromProps(this.props),
81 commentSort: CommentSortType.Hot,
82 commentViewType: CommentViewType.Tree,
86 siteRes: this.isoData.site_res,
90 constructor(props: any, context: any) {
91 super(props, context);
93 this.state = this.emptyState;
95 this.parseMessage = this.parseMessage.bind(this);
96 this.subscription = wsSubscribe(this.parseMessage);
98 // Only fetch the data if coming from another route
99 if (this.isoData.path == this.context.router.route.match.url) {
100 this.state.postRes = this.isoData.routeData[0];
101 this.state.categories = this.isoData.routeData[1].categories;
102 this.state.loading = false;
105 this.fetchCrossPosts();
106 if (this.state.commentId) {
107 this.scrollCommentIntoView();
112 WebSocketService.Instance.send(wsClient.listCategories());
117 let form: GetPost = {
118 id: this.state.postId,
119 auth: authField(false),
121 WebSocketService.Instance.send(wsClient.getPost(form));
125 if (this.state.postRes.post_view.post.url) {
127 q: this.state.postRes.post_view.post.url,
128 type_: SearchType.Url,
129 sort: SortType.TopAll,
132 auth: authField(false),
134 WebSocketService.Instance.send(wsClient.search(form));
138 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
139 let pathSplit = req.path.split('/');
140 let promises: Promise<any>[] = [];
142 let id = Number(pathSplit[2]);
144 let postForm: GetPost = {
147 setOptionalAuth(postForm, req.auth);
149 promises.push(req.client.getPost(postForm));
150 promises.push(req.client.listCategories());
155 componentWillUnmount() {
156 this.subscription.unsubscribe();
157 window.isoData.path = undefined;
160 componentDidMount() {
161 WebSocketService.Instance.send(
162 wsClient.postJoin({ post_id: this.state.postId })
164 autosize(document.querySelectorAll('textarea'));
167 componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
169 this.state.commentId &&
170 !this.state.scrolled &&
172 lastState.postRes.comments.length > 0
174 this.scrollCommentIntoView();
177 // Necessary if you are on a post and you click another post (same route)
178 if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
179 // TODO Couldnt get a refresh working. This does for now.
182 // let currentId = this.props.match.params.id;
183 // WebSocketService.Instance.getPost(currentId);
184 // this.context.refresh();
185 // this.context.router.history.push(_lastProps.location.pathname);
189 scrollCommentIntoView() {
190 var elmnt = document.getElementById(`comment-${this.state.commentId}`);
191 elmnt.scrollIntoView();
192 elmnt.classList.add('mark');
193 this.state.scrolled = true;
194 this.markScrolledAsRead(this.state.commentId);
197 // TODO this needs some re-work
198 markScrolledAsRead(commentId: number) {
199 let found = this.state.postRes.comments.find(
200 c => c.comment.id == commentId
202 let parent = this.state.postRes.comments.find(
203 c => found.comment.parent_id == c.comment.id
205 let parent_user_id = parent
207 : this.state.postRes.post_view.creator.id;
210 UserService.Instance.user &&
211 UserService.Instance.user.id == parent_user_id
213 let form: MarkCommentAsRead = {
214 comment_id: found.comment.id,
218 WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
219 UserService.Instance.unreadCountSub.next(
220 UserService.Instance.unreadCountSub.value - 1
225 get documentTitle(): string {
226 return `${this.state.postRes.post_view.post.name} - ${this.state.siteRes.site_view.site.name}`;
229 get imageTag(): string {
230 let post = this.state.postRes.post_view.post;
232 post.thumbnail_url ||
233 (post.url ? (isImage(post.url) ? post.url : undefined) : undefined)
237 get descriptionTag(): string {
238 let body = this.state.postRes.post_view.post.body;
239 return body ? previewLines(body) : undefined;
243 let pv = this.state.postRes?.post_view;
245 <div class="container">
246 {this.state.loading ? (
248 <svg class="icon icon-spinner spin">
249 <use xlinkHref="#icon-spinner"></use>
254 <div class="col-12 col-md-8 mb-3">
256 title={this.documentTitle}
257 path={this.context.router.route.match.url}
258 image={this.imageTag}
259 description={this.descriptionTag}
263 duplicates={this.state.crossPosts}
266 moderators={this.state.postRes.moderators}
267 admins={this.state.siteRes.admins}
269 this.state.siteRes.site_view.site.enable_downvotes
271 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
273 <div className="mb-2" />
275 postId={this.state.postId}
276 disabled={pv.post.locked}
278 {this.state.postRes.comments.length > 0 && this.sortRadios()}
279 {this.state.commentViewType == CommentViewType.Tree &&
281 {this.state.commentViewType == CommentViewType.Chat &&
284 <div class="col-12 col-sm-12 col-md-4">{this.sidebar()}</div>
294 <div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
296 className={`btn btn-outline-secondary pointer ${
297 this.state.commentSort === CommentSortType.Hot && 'active'
303 value={CommentSortType.Hot}
304 checked={this.state.commentSort === CommentSortType.Hot}
305 onChange={linkEvent(this, this.handleCommentSortChange)}
309 className={`btn btn-outline-secondary pointer ${
310 this.state.commentSort === CommentSortType.Top && 'active'
316 value={CommentSortType.Top}
317 checked={this.state.commentSort === CommentSortType.Top}
318 onChange={linkEvent(this, this.handleCommentSortChange)}
322 className={`btn btn-outline-secondary pointer ${
323 this.state.commentSort === CommentSortType.New && 'active'
329 value={CommentSortType.New}
330 checked={this.state.commentSort === CommentSortType.New}
331 onChange={linkEvent(this, this.handleCommentSortChange)}
335 className={`btn btn-outline-secondary pointer ${
336 this.state.commentSort === CommentSortType.Old && 'active'
342 value={CommentSortType.Old}
343 checked={this.state.commentSort === CommentSortType.Old}
344 onChange={linkEvent(this, this.handleCommentSortChange)}
348 <div class="btn-group btn-group-toggle flex-wrap mb-2">
350 className={`btn btn-outline-secondary pointer ${
351 this.state.commentViewType === CommentViewType.Chat && 'active'
357 value={CommentViewType.Chat}
358 checked={this.state.commentViewType === CommentViewType.Chat}
359 onChange={linkEvent(this, this.handleCommentViewTypeChange)}
371 nodes={commentsToFlatNodes(this.state.postRes.comments)}
373 locked={this.state.postRes.post_view.post.locked}
374 moderators={this.state.postRes.moderators}
375 admins={this.state.siteRes.admins}
376 postCreatorId={this.state.postRes.post_view.creator.id}
378 enableDownvotes={this.state.siteRes.site_view.site.enable_downvotes}
379 sort={this.state.commentSort}
389 community_view={this.state.postRes.community_view}
390 moderators={this.state.postRes.moderators}
391 admins={this.state.siteRes.admins}
392 online={this.state.postRes.online}
393 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
395 categories={this.state.categories}
401 handleCommentSortChange(i: Post, event: any) {
402 i.state.commentSort = Number(event.target.value);
403 i.state.commentViewType = CommentViewType.Tree;
407 handleCommentViewTypeChange(i: Post, event: any) {
408 i.state.commentViewType = Number(event.target.value);
409 i.state.commentSort = CommentSortType.New;
413 buildCommentsTree(): CommentNodeI[] {
414 let map = new Map<number, CommentNodeI>();
415 for (let comment_view of this.state.postRes.comments) {
416 let node: CommentNodeI = {
417 comment_view: comment_view,
420 map.set(comment_view.comment.id, { ...node });
422 let tree: CommentNodeI[] = [];
423 for (let comment_view of this.state.postRes.comments) {
424 let child = map.get(comment_view.comment.id);
425 if (comment_view.comment.parent_id) {
426 let parent_ = map.get(comment_view.comment.parent_id);
427 parent_.children.push(child);
432 this.setDepth(child);
438 setDepth(node: CommentNodeI, i: number = 0): void {
439 for (let child of node.children) {
441 this.setDepth(child, i + 1);
446 let nodes = this.buildCommentsTree();
451 locked={this.state.postRes.post_view.post.locked}
452 moderators={this.state.postRes.moderators}
453 admins={this.state.siteRes.admins}
454 postCreatorId={this.state.postRes.post_view.creator.id}
455 sort={this.state.commentSort}
456 enableDownvotes={this.state.siteRes.site_view.site.enable_downvotes}
462 parseMessage(msg: any) {
463 let op = wsUserOp(msg);
466 toast(i18n.t(msg.error), 'danger');
468 } else if (msg.reconnect) {
469 let postId = Number(this.props.match.params.id);
470 WebSocketService.Instance.send(wsClient.postJoin({ post_id: postId }));
471 WebSocketService.Instance.send(
474 auth: authField(false),
477 } else if (op == UserOperation.GetPost) {
478 let data = wsJsonToRes<GetPostResponse>(msg).data;
479 this.state.postRes = data;
480 this.state.loading = false;
483 this.fetchCrossPosts();
484 this.setState(this.state);
486 } else if (op == UserOperation.CreateComment) {
487 let data = wsJsonToRes<CommentResponse>(msg).data;
489 // Necessary since it might be a user reply, which has the recipients, to avoid double
490 if (data.recipient_ids.length == 0) {
491 this.state.postRes.comments.unshift(data.comment_view);
492 this.state.postRes.post_view.counts.comments++;
493 this.setState(this.state);
497 op == UserOperation.EditComment ||
498 op == UserOperation.DeleteComment ||
499 op == UserOperation.RemoveComment
501 let data = wsJsonToRes<CommentResponse>(msg).data;
502 editCommentRes(data.comment_view, this.state.postRes.comments);
503 this.setState(this.state);
504 } else if (op == UserOperation.SaveComment) {
505 let data = wsJsonToRes<CommentResponse>(msg).data;
506 saveCommentRes(data.comment_view, this.state.postRes.comments);
507 this.setState(this.state);
509 } else if (op == UserOperation.CreateCommentLike) {
510 let data = wsJsonToRes<CommentResponse>(msg).data;
511 createCommentLikeRes(data.comment_view, this.state.postRes.comments);
512 this.setState(this.state);
513 } else if (op == UserOperation.CreatePostLike) {
514 let data = wsJsonToRes<PostResponse>(msg).data;
515 createPostLikeRes(data.post_view, this.state.postRes.post_view);
516 this.setState(this.state);
518 op == UserOperation.EditPost ||
519 op == UserOperation.DeletePost ||
520 op == UserOperation.RemovePost ||
521 op == UserOperation.LockPost ||
522 op == UserOperation.StickyPost ||
523 op == UserOperation.SavePost
525 let data = wsJsonToRes<PostResponse>(msg).data;
526 this.state.postRes.post_view = data.post_view;
527 this.setState(this.state);
530 op == UserOperation.EditCommunity ||
531 op == UserOperation.DeleteCommunity ||
532 op == UserOperation.RemoveCommunity ||
533 op == UserOperation.FollowCommunity
535 let data = wsJsonToRes<CommunityResponse>(msg).data;
536 this.state.postRes.community_view = data.community_view;
537 this.state.postRes.post_view.community = data.community_view.community;
538 this.setState(this.state);
539 this.setState(this.state);
540 } else if (op == UserOperation.BanFromCommunity) {
541 let data = wsJsonToRes<BanFromCommunityResponse>(msg).data;
542 this.state.postRes.comments
543 .filter(c => c.creator.id == data.user_view.user.id)
544 .forEach(c => (c.creator_banned_from_community = data.banned));
545 if (this.state.postRes.post_view.creator.id == data.user_view.user.id) {
546 this.state.postRes.post_view.creator_banned_from_community =
549 this.setState(this.state);
550 } else if (op == UserOperation.AddModToCommunity) {
551 let data = wsJsonToRes<AddModToCommunityResponse>(msg).data;
552 this.state.postRes.moderators = data.moderators;
553 this.setState(this.state);
554 } else if (op == UserOperation.BanUser) {
555 let data = wsJsonToRes<BanUserResponse>(msg).data;
556 this.state.postRes.comments
557 .filter(c => c.creator.id == data.user_view.user.id)
558 .forEach(c => (c.creator.banned = data.banned));
559 if (this.state.postRes.post_view.creator.id == data.user_view.user.id) {
560 this.state.postRes.post_view.creator.banned = data.banned;
562 this.setState(this.state);
563 } else if (op == UserOperation.AddAdmin) {
564 let data = wsJsonToRes<AddAdminResponse>(msg).data;
565 this.state.siteRes.admins = data.admins;
566 this.setState(this.state);
567 } else if (op == UserOperation.Search) {
568 let data = wsJsonToRes<SearchResponse>(msg).data;
569 this.state.crossPosts = data.posts.filter(
570 p => p.post.id != Number(this.props.match.params.id)
572 this.setState(this.state);
573 } else if (op == UserOperation.TransferSite) {
574 let data = wsJsonToRes<GetSiteResponse>(msg).data;
575 this.state.siteRes = data;
576 this.setState(this.state);
577 } else if (op == UserOperation.TransferCommunity) {
578 let data = wsJsonToRes<GetCommunityResponse>(msg).data;
579 this.state.postRes.community_view = data.community_view;
580 this.state.postRes.post_view.community = data.community_view.community;
581 this.state.postRes.moderators = data.moderators;
582 this.setState(this.state);
583 } else if (op == UserOperation.ListCategories) {
584 let data = wsJsonToRes<ListCategoriesResponse>(msg).data;
585 this.state.categories = data.categories;
586 this.setState(this.state);