1 import { Component, linkEvent } from "inferno";
2 import { HtmlTags } from "./html-tags";
3 import { Spinner } from "./icon";
4 import { Subscription } from "rxjs";
13 BanFromCommunityResponse,
15 AddModToCommunityResponse,
24 } from "lemmy-js-client";
29 CommentNode as CommentNodeI,
30 } from "../interfaces";
31 import { WebSocketService, UserService } from "../services";
43 getCommentIdFromProps,
53 restoreScrollPosition,
55 insertCommentIntoTree,
57 import { PostListing } from "./post-listing";
58 import { Sidebar } from "./sidebar";
59 import { CommentForm } from "./comment-form";
60 import { CommentNodes } from "./comment-nodes";
61 import autosize from "autosize";
62 import { i18n } from "../i18next";
65 postRes: GetPostResponse;
67 commentTree: CommentNodeI[];
69 commentSort: CommentSortType;
70 commentViewType: CommentViewType;
73 crossPosts: PostView[];
74 siteRes: GetSiteResponse;
77 export class Post extends Component<any, PostState> {
78 private subscription: Subscription;
79 private isoData = setIsoData(this.context);
80 private emptyState: PostState = {
82 postId: getIdFromProps(this.props),
84 commentId: getCommentIdFromProps(this.props),
85 commentSort: CommentSortType.Hot,
86 commentViewType: CommentViewType.Tree,
90 siteRes: this.isoData.site_res,
93 constructor(props: any, context: any) {
94 super(props, context);
96 this.state = this.emptyState;
98 this.parseMessage = this.parseMessage.bind(this);
99 this.subscription = wsSubscribe(this.parseMessage);
101 // Only fetch the data if coming from another route
102 if (this.isoData.path == this.context.router.route.match.url) {
103 this.state.postRes = this.isoData.routeData[0];
104 this.state.commentTree = buildCommentsTree(
105 this.state.postRes.comments,
106 this.state.commentSort
108 this.state.loading = false;
111 this.fetchCrossPosts();
112 if (this.state.commentId) {
113 this.scrollCommentIntoView();
122 let form: GetPost = {
123 id: this.state.postId,
124 auth: authField(false),
126 WebSocketService.Instance.send(wsClient.getPost(form));
130 if (this.state.postRes.post_view.post.url) {
132 q: this.state.postRes.post_view.post.url,
133 type_: SearchType.Url,
134 sort: SortType.TopAll,
137 auth: authField(false),
139 WebSocketService.Instance.send(wsClient.search(form));
143 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
144 let pathSplit = req.path.split("/");
145 let promises: Promise<any>[] = [];
147 let id = Number(pathSplit[2]);
149 let postForm: GetPost = {
152 setOptionalAuth(postForm, req.auth);
154 promises.push(req.client.getPost(postForm));
159 componentWillUnmount() {
160 this.subscription.unsubscribe();
161 window.isoData.path = undefined;
162 saveScrollPosition(this.context);
165 componentDidMount() {
166 WebSocketService.Instance.send(
167 wsClient.postJoin({ post_id: this.state.postId })
169 autosize(document.querySelectorAll("textarea"));
172 componentDidUpdate(_lastProps: any, lastState: PostState) {
174 this.state.commentId &&
175 !this.state.scrolled &&
177 lastState.postRes.comments.length > 0
179 this.scrollCommentIntoView();
182 // Necessary if you are on a post and you click another post (same route)
183 if (_lastProps.location.pathname !== _lastProps.history.location.pathname) {
184 // TODO Couldnt get a refresh working. This does for now.
187 // let currentId = this.props.match.params.id;
188 // WebSocketService.Instance.getPost(currentId);
189 // this.context.refresh();
190 // this.context.router.history.push(_lastProps.location.pathname);
194 scrollCommentIntoView() {
195 var elmnt = document.getElementById(`comment-${this.state.commentId}`);
196 elmnt.scrollIntoView();
197 elmnt.classList.add("mark");
198 this.state.scrolled = true;
199 this.markScrolledAsRead(this.state.commentId);
202 // TODO this needs some re-work
203 markScrolledAsRead(commentId: number) {
204 let found = this.state.postRes.comments.find(
205 c => c.comment.id == commentId
207 let parent = this.state.postRes.comments.find(
208 c => found.comment.parent_id == c.comment.id
210 let parent_person_id = parent
212 : this.state.postRes.post_view.creator.id;
215 UserService.Instance.localUserView &&
216 UserService.Instance.localUserView.person.id == parent_person_id
218 let form: MarkCommentAsRead = {
219 comment_id: found.comment.id,
223 WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
224 UserService.Instance.unreadCountSub.next(
225 UserService.Instance.unreadCountSub.value - 1
230 get documentTitle(): string {
231 return `${this.state.postRes.post_view.post.name} - ${this.state.siteRes.site_view.site.name}`;
234 get imageTag(): string {
235 let post = this.state.postRes.post_view.post;
237 post.thumbnail_url ||
238 (post.url ? (isImage(post.url) ? post.url : undefined) : undefined)
242 get descriptionTag(): string {
243 let body = this.state.postRes.post_view.post.body;
244 return body ? previewLines(body) : undefined;
248 let pv = this.state.postRes?.post_view;
250 <div class="container">
251 {this.state.loading ? (
257 <div class="col-12 col-md-8 mb-3">
259 title={this.documentTitle}
260 path={this.context.router.route.match.url}
261 image={this.imageTag}
262 description={this.descriptionTag}
266 duplicates={this.state.crossPosts}
269 moderators={this.state.postRes.moderators}
270 admins={this.state.siteRes.admins}
272 this.state.siteRes.site_view.site.enable_downvotes
274 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
276 <div className="mb-2" />
278 postId={this.state.postId}
279 disabled={pv.post.locked}
281 {this.state.postRes.comments.length > 0 && this.sortRadios()}
282 {this.state.commentViewType == CommentViewType.Tree &&
284 {this.state.commentViewType == CommentViewType.Chat &&
287 <div class="col-12 col-sm-12 col-md-4">{this.sidebar()}</div>
297 <div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
299 className={`btn btn-outline-secondary pointer ${
300 this.state.commentSort === CommentSortType.Hot && "active"
306 value={CommentSortType.Hot}
307 checked={this.state.commentSort === CommentSortType.Hot}
308 onChange={linkEvent(this, this.handleCommentSortChange)}
312 className={`btn btn-outline-secondary pointer ${
313 this.state.commentSort === CommentSortType.Top && "active"
319 value={CommentSortType.Top}
320 checked={this.state.commentSort === CommentSortType.Top}
321 onChange={linkEvent(this, this.handleCommentSortChange)}
325 className={`btn btn-outline-secondary pointer ${
326 this.state.commentSort === CommentSortType.New && "active"
332 value={CommentSortType.New}
333 checked={this.state.commentSort === CommentSortType.New}
334 onChange={linkEvent(this, this.handleCommentSortChange)}
338 className={`btn btn-outline-secondary pointer ${
339 this.state.commentSort === CommentSortType.Old && "active"
345 value={CommentSortType.Old}
346 checked={this.state.commentSort === CommentSortType.Old}
347 onChange={linkEvent(this, this.handleCommentSortChange)}
351 <div class="btn-group btn-group-toggle flex-wrap mb-2">
353 className={`btn btn-outline-secondary pointer ${
354 this.state.commentViewType === CommentViewType.Chat && "active"
360 value={CommentViewType.Chat}
361 checked={this.state.commentViewType === CommentViewType.Chat}
362 onChange={linkEvent(this, this.handleCommentViewTypeChange)}
371 // These are already sorted by new
375 nodes={commentsToFlatNodes(this.state.postRes.comments)}
377 locked={this.state.postRes.post_view.post.locked}
378 moderators={this.state.postRes.moderators}
379 admins={this.state.siteRes.admins}
380 postCreatorId={this.state.postRes.post_view.creator.id}
382 enableDownvotes={this.state.siteRes.site_view.site.enable_downvotes}
392 community_view={this.state.postRes.community_view}
393 moderators={this.state.postRes.moderators}
394 admins={this.state.siteRes.admins}
395 online={this.state.postRes.online}
396 enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
403 handleCommentSortChange(i: Post, event: any) {
404 i.state.commentSort = Number(event.target.value);
405 i.state.commentViewType = CommentViewType.Tree;
406 i.state.commentTree = buildCommentsTree(
407 i.state.postRes.comments,
413 handleCommentViewTypeChange(i: Post, event: any) {
414 i.state.commentViewType = Number(event.target.value);
415 i.state.commentSort = CommentSortType.New;
416 i.state.commentTree = buildCommentsTree(
417 i.state.postRes.comments,
427 nodes={this.state.commentTree}
428 locked={this.state.postRes.post_view.post.locked}
429 moderators={this.state.postRes.moderators}
430 admins={this.state.siteRes.admins}
431 postCreatorId={this.state.postRes.post_view.creator.id}
432 enableDownvotes={this.state.siteRes.site_view.site.enable_downvotes}
438 parseMessage(msg: any) {
439 let op = wsUserOp(msg);
442 toast(i18n.t(msg.error), "danger");
444 } else if (msg.reconnect) {
445 let postId = Number(this.props.match.params.id);
446 WebSocketService.Instance.send(wsClient.postJoin({ post_id: postId }));
447 WebSocketService.Instance.send(
450 auth: authField(false),
453 } else if (op == UserOperation.GetPost) {
454 let data = wsJsonToRes<GetPostResponse>(msg).data;
455 this.state.postRes = data;
456 this.state.commentTree = buildCommentsTree(
457 this.state.postRes.comments,
458 this.state.commentSort
460 this.state.loading = false;
463 this.fetchCrossPosts();
464 this.setState(this.state);
466 if (!this.state.commentId) restoreScrollPosition(this.context);
467 } else if (op == UserOperation.CreateComment) {
468 let data = wsJsonToRes<CommentResponse>(msg).data;
470 // Necessary since it might be a user reply, which has the recipients, to avoid double
471 if (data.recipient_ids.length == 0) {
472 this.state.postRes.comments.unshift(data.comment_view);
473 insertCommentIntoTree(this.state.commentTree, data.comment_view);
474 this.state.postRes.post_view.counts.comments++;
475 this.setState(this.state);
479 op == UserOperation.EditComment ||
480 op == UserOperation.DeleteComment ||
481 op == UserOperation.RemoveComment
483 let data = wsJsonToRes<CommentResponse>(msg).data;
484 editCommentRes(data.comment_view, this.state.postRes.comments);
485 this.setState(this.state);
486 } else if (op == UserOperation.SaveComment) {
487 let data = wsJsonToRes<CommentResponse>(msg).data;
488 saveCommentRes(data.comment_view, this.state.postRes.comments);
489 this.setState(this.state);
491 } else if (op == UserOperation.CreateCommentLike) {
492 let data = wsJsonToRes<CommentResponse>(msg).data;
493 createCommentLikeRes(data.comment_view, this.state.postRes.comments);
494 this.setState(this.state);
495 } else if (op == UserOperation.CreatePostLike) {
496 let data = wsJsonToRes<PostResponse>(msg).data;
497 createPostLikeRes(data.post_view, this.state.postRes.post_view);
498 this.setState(this.state);
500 op == UserOperation.EditPost ||
501 op == UserOperation.DeletePost ||
502 op == UserOperation.RemovePost ||
503 op == UserOperation.LockPost ||
504 op == UserOperation.StickyPost ||
505 op == UserOperation.SavePost
507 let data = wsJsonToRes<PostResponse>(msg).data;
508 this.state.postRes.post_view = data.post_view;
509 this.setState(this.state);
512 op == UserOperation.EditCommunity ||
513 op == UserOperation.DeleteCommunity ||
514 op == UserOperation.RemoveCommunity ||
515 op == UserOperation.FollowCommunity
517 let data = wsJsonToRes<CommunityResponse>(msg).data;
518 this.state.postRes.community_view = data.community_view;
519 this.state.postRes.post_view.community = data.community_view.community;
520 this.setState(this.state);
521 this.setState(this.state);
522 } else if (op == UserOperation.BanFromCommunity) {
523 let data = wsJsonToRes<BanFromCommunityResponse>(msg).data;
524 this.state.postRes.comments
525 .filter(c => c.creator.id == data.person_view.person.id)
526 .forEach(c => (c.creator_banned_from_community = data.banned));
528 this.state.postRes.post_view.creator.id == data.person_view.person.id
530 this.state.postRes.post_view.creator_banned_from_community =
533 this.setState(this.state);
534 } else if (op == UserOperation.AddModToCommunity) {
535 let data = wsJsonToRes<AddModToCommunityResponse>(msg).data;
536 this.state.postRes.moderators = data.moderators;
537 this.setState(this.state);
538 } else if (op == UserOperation.BanPerson) {
539 let data = wsJsonToRes<BanPersonResponse>(msg).data;
540 this.state.postRes.comments
541 .filter(c => c.creator.id == data.person_view.person.id)
542 .forEach(c => (c.creator.banned = data.banned));
544 this.state.postRes.post_view.creator.id == data.person_view.person.id
546 this.state.postRes.post_view.creator.banned = data.banned;
548 this.setState(this.state);
549 } else if (op == UserOperation.AddAdmin) {
550 let data = wsJsonToRes<AddAdminResponse>(msg).data;
551 this.state.siteRes.admins = data.admins;
552 this.setState(this.state);
553 } else if (op == UserOperation.Search) {
554 let data = wsJsonToRes<SearchResponse>(msg).data;
555 this.state.crossPosts = data.posts.filter(
556 p => p.post.id != Number(this.props.match.params.id)
558 this.setState(this.state);
559 } else if (op == UserOperation.TransferSite) {
560 let data = wsJsonToRes<GetSiteResponse>(msg).data;
561 this.state.siteRes = data;
562 this.setState(this.state);
563 } else if (op == UserOperation.TransferCommunity) {
564 let data = wsJsonToRes<GetCommunityResponse>(msg).data;
565 this.state.postRes.community_view = data.community_view;
566 this.state.postRes.post_view.community = data.community_view.community;
567 this.state.postRes.moderators = data.moderators;
568 this.setState(this.state);