1 import autosize from "autosize";
2 import { Component, createRef, linkEvent, RefObject } from "inferno";
5 AddModToCommunityResponse,
6 BanFromCommunityResponse,
26 } from "lemmy-js-client";
27 import { Subscription } from "rxjs";
28 import { i18n } from "../../i18next";
30 CommentNode as CommentNodeI,
34 } from "../../interfaces";
35 import { UserService, WebSocketService } from "../../services";
43 getCommentIdFromProps,
45 insertCommentIntoTree,
49 restoreScrollPosition,
62 import { CommentForm } from "../comment/comment-form";
63 import { CommentNodes } from "../comment/comment-nodes";
64 import { HtmlTags } from "../common/html-tags";
65 import { Icon, Spinner } from "../common/icon";
66 import { Sidebar } from "../community/sidebar";
67 import { PostListing } from "./post-listing";
70 postRes: GetPostResponse;
72 commentTree: CommentNodeI[];
74 commentSort: CommentSortType;
75 commentViewType: CommentViewType;
78 crossPosts: PostView[];
79 siteRes: GetSiteResponse;
80 commentSectionRef?: RefObject<HTMLDivElement>;
81 showSidebarMobile: boolean;
84 export class Post extends Component<any, PostState> {
85 private subscription: Subscription;
86 private isoData = setIsoData(this.context);
87 private emptyState: PostState = {
89 postId: getIdFromProps(this.props),
91 commentId: getCommentIdFromProps(this.props),
92 commentSort: CommentSortType.Hot,
93 commentViewType: CommentViewType.Tree,
97 siteRes: this.isoData.site_res,
98 commentSectionRef: null,
99 showSidebarMobile: false,
102 constructor(props: any, context: any) {
103 super(props, context);
105 this.state = this.emptyState;
106 this.state.commentSectionRef = createRef();
108 this.parseMessage = this.parseMessage.bind(this);
109 this.subscription = wsSubscribe(this.parseMessage);
111 // Only fetch the data if coming from another route
112 if (this.isoData.path == this.context.router.route.match.url) {
113 this.state.postRes = this.isoData.routeData[0];
114 this.state.commentTree = buildCommentsTree(
115 this.state.postRes.comments,
116 this.state.commentSort
118 this.state.loading = false;
121 this.fetchCrossPosts();
122 if (this.state.commentId) {
123 this.scrollCommentIntoView();
126 if (this.checkScrollIntoCommentsParam) {
127 this.scrollIntoCommentSection();
136 let form: GetPost = {
137 id: this.state.postId,
138 auth: authField(false),
140 WebSocketService.Instance.send(wsClient.getPost(form));
144 if (this.state.postRes.post_view.post.url) {
146 q: this.state.postRes.post_view.post.url,
147 type_: SearchType.Url,
148 sort: SortType.TopAll,
149 listing_type: ListingType.All,
152 auth: authField(false),
154 WebSocketService.Instance.send(wsClient.search(form));
158 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
159 let pathSplit = req.path.split("/");
160 let promises: Promise<any>[] = [];
162 let id = Number(pathSplit[2]);
164 let postForm: GetPost = {
167 setOptionalAuth(postForm, req.auth);
169 promises.push(req.client.getPost(postForm));
174 componentWillUnmount() {
175 this.subscription.unsubscribe();
176 window.isoData.path = undefined;
177 saveScrollPosition(this.context);
180 componentDidMount() {
181 WebSocketService.Instance.send(
182 wsClient.postJoin({ post_id: this.state.postId })
184 autosize(document.querySelectorAll("textarea"));
187 componentDidUpdate(_lastProps: any, lastState: PostState) {
189 this.state.commentId &&
190 !this.state.scrolled &&
192 lastState.postRes.comments.length > 0
194 this.scrollCommentIntoView();
197 // Necessary if you are on a post and you click another post (same route)
198 if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
199 // TODO Couldnt get a refresh working. This does for now.
202 // let currentId = this.props.match.params.id;
203 // WebSocketService.Instance.getPost(currentId);
204 // this.context.refresh();
205 // this.context.router.history.push(_lastProps.location.pathname);
209 scrollCommentIntoView() {
210 var elmnt = document.getElementById(`comment-${this.state.commentId}`);
211 elmnt.scrollIntoView();
212 elmnt.classList.add("mark");
213 this.state.scrolled = true;
214 this.markScrolledAsRead(this.state.commentId);
217 get checkScrollIntoCommentsParam() {
219 new URLSearchParams(this.props.location.search).get("scrollToComments")
223 scrollIntoCommentSection() {
224 this.state.commentSectionRef.current?.scrollIntoView();
227 // TODO this needs some re-work
228 markScrolledAsRead(commentId: number) {
229 let found = this.state.postRes.comments.find(
230 c => c.comment.id == commentId
232 let parent = this.state.postRes.comments.find(
233 c => found.comment.parent_id == c.comment.id
235 let parent_person_id = parent
237 : this.state.postRes.post_view.creator.id;
240 UserService.Instance.myUserInfo &&
241 UserService.Instance.myUserInfo.local_user_view.person.id ==
244 let form: MarkCommentAsRead = {
245 comment_id: found.comment.id,
249 WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
250 UserService.Instance.unreadInboxCountSub.next(
251 UserService.Instance.unreadInboxCountSub.value - 1
256 get documentTitle(): string {
257 return `${this.state.postRes.post_view.post.name} - ${this.state.siteRes.site_view.site.name}`;
260 get imageTag(): string {
261 let post = this.state.postRes.post_view.post;
263 post.thumbnail_url ||
264 (post.url ? (isImage(post.url) ? post.url : undefined) : undefined)
268 get descriptionTag(): string {
269 let body = this.state.postRes.post_view.post.body;
270 return body ? previewLines(body) : undefined;
274 let pv = this.state.postRes?.post_view;
276 <div class="container">
277 {this.state.loading ? (
283 <div class="col-12 col-md-8 mb-3">
285 title={this.documentTitle}
286 path={this.context.router.route.match.url}
287 image={this.imageTag}
288 description={this.descriptionTag}
292 duplicates={this.state.crossPosts}
295 moderators={this.state.postRes.moderators}
296 admins={this.state.siteRes.admins}
298 this.state.siteRes.site_view.site.enable_downvotes
300 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
302 <div ref={this.state.commentSectionRef} className="mb-2" />
304 postId={this.state.postId}
305 disabled={pv.post.locked}
307 <div class="d-block d-md-none">
309 class="btn btn-secondary d-inline-block mb-2 mr-3"
310 onClick={linkEvent(this, this.handleShowSidebarMobile)}
312 {i18n.t("sidebar")}{" "}
315 this.state.showSidebarMobile
319 classes="icon-inline"
322 {this.state.showSidebarMobile && this.sidebar()}
324 {this.state.postRes.comments.length > 0 && this.sortRadios()}
325 {this.state.commentViewType == CommentViewType.Tree &&
327 {this.state.commentViewType == CommentViewType.Chat &&
330 <div class="d-none d-md-block col-md-4">{this.sidebar()}</div>
340 <div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
342 className={`btn btn-outline-secondary pointer ${
343 this.state.commentSort === CommentSortType.Hot && "active"
349 value={CommentSortType.Hot}
350 checked={this.state.commentSort === CommentSortType.Hot}
351 onChange={linkEvent(this, this.handleCommentSortChange)}
355 className={`btn btn-outline-secondary pointer ${
356 this.state.commentSort === CommentSortType.Top && "active"
362 value={CommentSortType.Top}
363 checked={this.state.commentSort === CommentSortType.Top}
364 onChange={linkEvent(this, this.handleCommentSortChange)}
368 className={`btn btn-outline-secondary pointer ${
369 this.state.commentSort === CommentSortType.New && "active"
375 value={CommentSortType.New}
376 checked={this.state.commentSort === CommentSortType.New}
377 onChange={linkEvent(this, this.handleCommentSortChange)}
381 className={`btn btn-outline-secondary pointer ${
382 this.state.commentSort === CommentSortType.Old && "active"
388 value={CommentSortType.Old}
389 checked={this.state.commentSort === CommentSortType.Old}
390 onChange={linkEvent(this, this.handleCommentSortChange)}
394 <div class="btn-group btn-group-toggle flex-wrap mb-2">
396 className={`btn btn-outline-secondary pointer ${
397 this.state.commentViewType === CommentViewType.Chat && "active"
403 value={CommentViewType.Chat}
404 checked={this.state.commentViewType === CommentViewType.Chat}
405 onChange={linkEvent(this, this.handleCommentViewTypeChange)}
414 // These are already sorted by new
418 nodes={commentsToFlatNodes(this.state.postRes.comments)}
420 locked={this.state.postRes.post_view.post.locked}
421 moderators={this.state.postRes.moderators}
422 admins={this.state.siteRes.admins}
423 postCreatorId={this.state.postRes.post_view.creator.id}
425 enableDownvotes={this.state.siteRes.site_view.site.enable_downvotes}
435 community_view={this.state.postRes.community_view}
436 moderators={this.state.postRes.moderators}
437 admins={this.state.siteRes.admins}
438 online={this.state.postRes.online}
439 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
446 handleCommentSortChange(i: Post, event: any) {
447 i.state.commentSort = Number(event.target.value);
448 i.state.commentViewType = CommentViewType.Tree;
449 i.state.commentTree = buildCommentsTree(
450 i.state.postRes.comments,
456 handleCommentViewTypeChange(i: Post, event: any) {
457 i.state.commentViewType = Number(event.target.value);
458 i.state.commentSort = CommentSortType.New;
459 i.state.commentTree = buildCommentsTree(
460 i.state.postRes.comments,
466 handleShowSidebarMobile(i: Post) {
467 i.state.showSidebarMobile = !i.state.showSidebarMobile;
475 nodes={this.state.commentTree}
476 locked={this.state.postRes.post_view.post.locked}
477 moderators={this.state.postRes.moderators}
478 admins={this.state.siteRes.admins}
479 postCreatorId={this.state.postRes.post_view.creator.id}
480 enableDownvotes={this.state.siteRes.site_view.site.enable_downvotes}
486 parseMessage(msg: any) {
487 let op = wsUserOp(msg);
490 toast(i18n.t(msg.error), "danger");
492 } else if (msg.reconnect) {
493 let postId = Number(this.props.match.params.id);
494 WebSocketService.Instance.send(wsClient.postJoin({ post_id: postId }));
495 WebSocketService.Instance.send(
498 auth: authField(false),
501 } else if (op == UserOperation.GetPost) {
502 let data = wsJsonToRes<GetPostResponse>(msg).data;
503 this.state.postRes = data;
504 this.state.commentTree = buildCommentsTree(
505 this.state.postRes.comments,
506 this.state.commentSort
508 this.state.loading = false;
511 this.fetchCrossPosts();
512 this.setState(this.state);
514 if (!this.state.commentId) restoreScrollPosition(this.context);
516 if (this.checkScrollIntoCommentsParam) {
517 this.scrollIntoCommentSection();
519 } else if (op == UserOperation.CreateComment) {
520 let data = wsJsonToRes<CommentResponse>(msg).data;
522 // Necessary since it might be a user reply, which has the recipients, to avoid double
523 if (data.recipient_ids.length == 0) {
524 this.state.postRes.comments.unshift(data.comment_view);
525 insertCommentIntoTree(this.state.commentTree, data.comment_view);
526 this.state.postRes.post_view.counts.comments++;
527 this.setState(this.state);
531 op == UserOperation.EditComment ||
532 op == UserOperation.DeleteComment ||
533 op == UserOperation.RemoveComment
535 let data = wsJsonToRes<CommentResponse>(msg).data;
536 editCommentRes(data.comment_view, this.state.postRes.comments);
537 this.setState(this.state);
538 } else if (op == UserOperation.SaveComment) {
539 let data = wsJsonToRes<CommentResponse>(msg).data;
540 saveCommentRes(data.comment_view, this.state.postRes.comments);
541 this.setState(this.state);
543 } else if (op == UserOperation.CreateCommentLike) {
544 let data = wsJsonToRes<CommentResponse>(msg).data;
545 createCommentLikeRes(data.comment_view, this.state.postRes.comments);
546 this.setState(this.state);
547 } else if (op == UserOperation.CreatePostLike) {
548 let data = wsJsonToRes<PostResponse>(msg).data;
549 createPostLikeRes(data.post_view, this.state.postRes.post_view);
550 this.setState(this.state);
552 op == UserOperation.EditPost ||
553 op == UserOperation.DeletePost ||
554 op == UserOperation.RemovePost ||
555 op == UserOperation.LockPost ||
556 op == UserOperation.StickyPost ||
557 op == UserOperation.SavePost
559 let data = wsJsonToRes<PostResponse>(msg).data;
560 this.state.postRes.post_view = data.post_view;
561 this.setState(this.state);
564 op == UserOperation.EditCommunity ||
565 op == UserOperation.DeleteCommunity ||
566 op == UserOperation.RemoveCommunity ||
567 op == UserOperation.FollowCommunity
569 let data = wsJsonToRes<CommunityResponse>(msg).data;
570 this.state.postRes.community_view = data.community_view;
571 this.state.postRes.post_view.community = data.community_view.community;
572 this.setState(this.state);
573 this.setState(this.state);
574 } else if (op == UserOperation.BanFromCommunity) {
575 let data = wsJsonToRes<BanFromCommunityResponse>(msg).data;
576 this.state.postRes.comments
577 .filter(c => c.creator.id == data.person_view.person.id)
578 .forEach(c => (c.creator_banned_from_community = data.banned));
580 this.state.postRes.post_view.creator.id == data.person_view.person.id
582 this.state.postRes.post_view.creator_banned_from_community =
585 this.setState(this.state);
586 } else if (op == UserOperation.AddModToCommunity) {
587 let data = wsJsonToRes<AddModToCommunityResponse>(msg).data;
588 this.state.postRes.moderators = data.moderators;
589 this.setState(this.state);
590 } else if (op == UserOperation.BanPerson) {
591 let data = wsJsonToRes<BanPersonResponse>(msg).data;
592 this.state.postRes.comments
593 .filter(c => c.creator.id == data.person_view.person.id)
594 .forEach(c => (c.creator.banned = data.banned));
596 this.state.postRes.post_view.creator.id == data.person_view.person.id
598 this.state.postRes.post_view.creator.banned = data.banned;
600 this.setState(this.state);
601 } else if (op == UserOperation.AddAdmin) {
602 let data = wsJsonToRes<AddAdminResponse>(msg).data;
603 this.state.siteRes.admins = data.admins;
604 this.setState(this.state);
605 } else if (op == UserOperation.Search) {
606 let data = wsJsonToRes<SearchResponse>(msg).data;
607 this.state.crossPosts = data.posts.filter(
608 p => p.post.id != Number(this.props.match.params.id)
610 this.setState(this.state);
611 } else if (op == UserOperation.TransferSite) {
612 let data = wsJsonToRes<GetSiteResponse>(msg).data;
613 this.state.siteRes = data;
614 this.setState(this.state);
615 } else if (op == UserOperation.TransferCommunity) {
616 let data = wsJsonToRes<GetCommunityResponse>(msg).data;
617 this.state.postRes.community_view = data.community_view;
618 this.state.postRes.post_view.community = data.community_view.community;
619 this.state.postRes.moderators = data.moderators;
620 this.setState(this.state);
621 } else if (op == UserOperation.BlockPerson) {
622 let data = wsJsonToRes<BlockPersonResponse>(msg).data;
623 updatePersonBlock(data);
624 } else if (op == UserOperation.CreatePostReport) {
625 let data = wsJsonToRes<PostReportResponse>(msg).data;
627 toast(i18n.t("report_created"));
629 } else if (op == UserOperation.CreateCommentReport) {
630 let data = wsJsonToRes<CommentReportResponse>(msg).data;
632 toast(i18n.t("report_created"));