import autosize from "autosize"; import { Component, createRef, linkEvent, RefObject } from "inferno"; import { AddAdminResponse, AddModToCommunityResponse, BanFromCommunityResponse, BanPersonResponse, BlockPersonResponse, CommentReportResponse, CommentResponse, CommunityResponse, GetCommunityResponse, GetPost, GetPostResponse, GetSiteResponse, ListingType, MarkCommentAsRead, PostReportResponse, PostResponse, PostView, Search, SearchResponse, SearchType, SortType, UserOperation, } from "lemmy-js-client"; import { Subscription } from "rxjs"; import { i18n } from "../../i18next"; import { CommentNode as CommentNodeI, CommentSortType, CommentViewType, InitialFetchRequest, } from "../../interfaces"; import { UserService, WebSocketService } from "../../services"; import { authField, buildCommentsTree, commentsToFlatNodes, createCommentLikeRes, createPostLikeRes, debounce, editCommentRes, getCommentIdFromProps, getIdFromProps, insertCommentIntoTree, isBrowser, isImage, previewLines, restoreScrollPosition, saveCommentRes, saveScrollPosition, setIsoData, setOptionalAuth, setupTippy, toast, updatePersonBlock, wsClient, wsJsonToRes, wsSubscribe, wsUserOp, } from "../../utils"; import { CommentForm } from "../comment/comment-form"; import { CommentNodes } from "../comment/comment-nodes"; import { HtmlTags } from "../common/html-tags"; import { Icon, Spinner } from "../common/icon"; import { Sidebar } from "../community/sidebar"; import { PostListing } from "./post-listing"; const commentsShownInterval = 15; interface PostState { postRes: GetPostResponse; postId: number; commentTree: CommentNodeI[]; commentId?: number; commentSort: CommentSortType; commentViewType: CommentViewType; scrolled?: boolean; loading: boolean; crossPosts: PostView[]; siteRes: GetSiteResponse; commentSectionRef?: RefObject; showSidebarMobile: boolean; maxCommentsShown: number; } export class Post extends Component { private subscription: Subscription; private isoData = setIsoData(this.context); private commentScrollDebounced: () => void; private emptyState: PostState = { postRes: null, postId: getIdFromProps(this.props), commentTree: [], commentId: getCommentIdFromProps(this.props), commentSort: CommentSortType.Hot, commentViewType: CommentViewType.Tree, scrolled: false, loading: true, crossPosts: [], siteRes: this.isoData.site_res, commentSectionRef: null, showSidebarMobile: false, maxCommentsShown: commentsShownInterval, }; constructor(props: any, context: any) { super(props, context); this.state = this.emptyState; this.state.commentSectionRef = createRef(); this.parseMessage = this.parseMessage.bind(this); this.subscription = wsSubscribe(this.parseMessage); // Only fetch the data if coming from another route if (this.isoData.path == this.context.router.route.match.url) { this.state.postRes = this.isoData.routeData[0]; this.state.commentTree = buildCommentsTree( this.state.postRes.comments, this.state.commentSort ); this.state.loading = false; if (isBrowser()) { this.fetchCrossPosts(); if (this.state.commentId) { this.scrollCommentIntoView(); } if (this.checkScrollIntoCommentsParam) { this.scrollIntoCommentSection(); } } } else { this.fetchPost(); } } fetchPost() { let form: GetPost = { id: this.state.postId, auth: authField(false), }; WebSocketService.Instance.send(wsClient.getPost(form)); } fetchCrossPosts() { if (this.state.postRes.post_view.post.url) { let form: Search = { q: this.state.postRes.post_view.post.url, type_: SearchType.Url, sort: SortType.TopAll, listing_type: ListingType.All, page: 1, limit: 6, auth: authField(false), }; WebSocketService.Instance.send(wsClient.search(form)); } } static fetchInitialData(req: InitialFetchRequest): Promise[] { let pathSplit = req.path.split("/"); let promises: Promise[] = []; let id = Number(pathSplit[2]); let postForm: GetPost = { id, }; setOptionalAuth(postForm, req.auth); promises.push(req.client.getPost(postForm)); return promises; } componentWillUnmount() { this.subscription.unsubscribe(); document.removeEventListener("scroll", this.commentScrollDebounced); window.isoData.path = undefined; saveScrollPosition(this.context); } componentDidMount() { WebSocketService.Instance.send( wsClient.postJoin({ post_id: this.state.postId }) ); autosize(document.querySelectorAll("textarea")); this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100); document.addEventListener("scroll", this.commentScrollDebounced); } componentDidUpdate(_lastProps: any) { // Necessary if you are on a post and you click another post (same route) if (_lastProps.location.pathname !== _lastProps.history.location.pathname) { // TODO Couldnt get a refresh working. This does for now. location.reload(); // let currentId = this.props.match.params.id; // WebSocketService.Instance.getPost(currentId); // this.context.refresh(); // this.context.router.history.push(_lastProps.location.pathname); } } scrollCommentIntoView() { let commentElement = document.getElementById( `comment-${this.state.commentId}` ); if (commentElement) { commentElement.scrollIntoView(); commentElement.classList.add("mark"); this.state.scrolled = true; this.markScrolledAsRead(this.state.commentId); } } get checkScrollIntoCommentsParam() { return Boolean( new URLSearchParams(this.props.location.search).get("scrollToComments") ); } scrollIntoCommentSection() { this.state.commentSectionRef.current?.scrollIntoView(); } // TODO this needs some re-work markScrolledAsRead(commentId: number) { let found = this.state.postRes.comments.find( c => c.comment.id == commentId ); let parent = this.state.postRes.comments.find( c => found.comment.parent_id == c.comment.id ); let parent_person_id = parent ? parent.creator.id : this.state.postRes.post_view.creator.id; if ( UserService.Instance.myUserInfo && UserService.Instance.myUserInfo.local_user_view.person.id == parent_person_id ) { let form: MarkCommentAsRead = { comment_id: found.comment.id, read: true, auth: authField(), }; WebSocketService.Instance.send(wsClient.markCommentAsRead(form)); UserService.Instance.unreadInboxCountSub.next( UserService.Instance.unreadInboxCountSub.value - 1 ); } } isBottom(el: Element) { return el?.getBoundingClientRect().bottom <= window.innerHeight; } /** * Shows new comments when scrolling to the bottom of the comments div */ trackCommentsBoxScrolling = () => { const wrappedElement = document.getElementsByClassName("comments")[0]; if (wrappedElement && this.isBottom(wrappedElement)) { this.state.maxCommentsShown += commentsShownInterval; this.setState(this.state); } }; get documentTitle(): string { return `${this.state.postRes.post_view.post.name} - ${this.state.siteRes.site_view.site.name}`; } get imageTag(): string { let post = this.state.postRes.post_view.post; return ( post.thumbnail_url || (post.url ? (isImage(post.url) ? post.url : undefined) : undefined) ); } get descriptionTag(): string { let body = this.state.postRes.post_view.post.body; return body ? previewLines(body) : undefined; } render() { let pv = this.state.postRes?.post_view; return (
{this.state.loading ? (
) : (
{this.state.showSidebarMobile && this.sidebar()}
{this.state.postRes.comments.length > 0 && this.sortRadios()} {this.state.commentViewType == CommentViewType.Tree && this.commentsTree()} {this.state.commentViewType == CommentViewType.Chat && this.commentsFlat()}
{this.sidebar()}
)}
); } sortRadios() { return ( <>
); } commentsFlat() { // These are already sorted by new return (
); } sidebar() { return (
); } handleCommentSortChange(i: Post, event: any) { i.state.commentSort = Number(event.target.value); i.state.commentViewType = CommentViewType.Tree; i.state.commentTree = buildCommentsTree( i.state.postRes.comments, i.state.commentSort ); i.setState(i.state); } handleCommentViewTypeChange(i: Post, event: any) { i.state.commentViewType = Number(event.target.value); i.state.commentSort = CommentSortType.New; i.state.commentTree = buildCommentsTree( i.state.postRes.comments, i.state.commentSort ); i.setState(i.state); } handleShowSidebarMobile(i: Post) { i.state.showSidebarMobile = !i.state.showSidebarMobile; i.setState(i.state); } commentsTree() { return (
); } parseMessage(msg: any) { let op = wsUserOp(msg); console.log(msg); if (msg.error) { toast(i18n.t(msg.error), "danger"); return; } else if (msg.reconnect) { let postId = Number(this.props.match.params.id); WebSocketService.Instance.send(wsClient.postJoin({ post_id: postId })); WebSocketService.Instance.send( wsClient.getPost({ id: postId, auth: authField(false), }) ); } else if (op == UserOperation.GetPost) { let data = wsJsonToRes(msg).data; this.state.postRes = data; this.state.commentTree = buildCommentsTree( this.state.postRes.comments, this.state.commentSort ); this.state.loading = false; // Get cross-posts this.fetchCrossPosts(); this.setState(this.state); setupTippy(); if (!this.state.commentId) restoreScrollPosition(this.context); if (this.checkScrollIntoCommentsParam) { this.scrollIntoCommentSection(); } if (this.state.commentId && !this.state.scrolled) { this.scrollCommentIntoView(); } } else if (op == UserOperation.CreateComment) { let data = wsJsonToRes(msg).data; // Don't get comments from the post room, if the creator is blocked let creatorBlocked = UserService.Instance.myUserInfo?.person_blocks .map(pb => pb.target.id) .includes(data.comment_view.creator.id); // Necessary since it might be a user reply, which has the recipients, to avoid double if (data.recipient_ids.length == 0 && !creatorBlocked) { this.state.postRes.comments.unshift(data.comment_view); insertCommentIntoTree(this.state.commentTree, data.comment_view); this.state.postRes.post_view.counts.comments++; this.setState(this.state); setupTippy(); } } else if ( op == UserOperation.EditComment || op == UserOperation.DeleteComment || op == UserOperation.RemoveComment ) { let data = wsJsonToRes(msg).data; editCommentRes(data.comment_view, this.state.postRes.comments); this.setState(this.state); } else if (op == UserOperation.SaveComment) { let data = wsJsonToRes(msg).data; saveCommentRes(data.comment_view, this.state.postRes.comments); this.setState(this.state); setupTippy(); } else if (op == UserOperation.CreateCommentLike) { let data = wsJsonToRes(msg).data; createCommentLikeRes(data.comment_view, this.state.postRes.comments); this.setState(this.state); } else if (op == UserOperation.CreatePostLike) { let data = wsJsonToRes(msg).data; createPostLikeRes(data.post_view, this.state.postRes.post_view); this.setState(this.state); } else if ( op == UserOperation.EditPost || op == UserOperation.DeletePost || op == UserOperation.RemovePost || op == UserOperation.LockPost || op == UserOperation.StickyPost || op == UserOperation.SavePost ) { let data = wsJsonToRes(msg).data; this.state.postRes.post_view = data.post_view; this.setState(this.state); setupTippy(); } else if ( op == UserOperation.EditCommunity || op == UserOperation.DeleteCommunity || op == UserOperation.RemoveCommunity || op == UserOperation.FollowCommunity ) { let data = wsJsonToRes(msg).data; this.state.postRes.community_view = data.community_view; this.state.postRes.post_view.community = data.community_view.community; this.setState(this.state); this.setState(this.state); } else if (op == UserOperation.BanFromCommunity) { let data = wsJsonToRes(msg).data; this.state.postRes.comments .filter(c => c.creator.id == data.person_view.person.id) .forEach(c => (c.creator_banned_from_community = data.banned)); if ( this.state.postRes.post_view.creator.id == data.person_view.person.id ) { this.state.postRes.post_view.creator_banned_from_community = data.banned; } this.setState(this.state); } else if (op == UserOperation.AddModToCommunity) { let data = wsJsonToRes(msg).data; this.state.postRes.moderators = data.moderators; this.setState(this.state); } else if (op == UserOperation.BanPerson) { let data = wsJsonToRes(msg).data; this.state.postRes.comments .filter(c => c.creator.id == data.person_view.person.id) .forEach(c => (c.creator.banned = data.banned)); if ( this.state.postRes.post_view.creator.id == data.person_view.person.id ) { this.state.postRes.post_view.creator.banned = data.banned; } this.setState(this.state); } else if (op == UserOperation.AddAdmin) { let data = wsJsonToRes(msg).data; this.state.siteRes.admins = data.admins; this.setState(this.state); } else if (op == UserOperation.Search) { let data = wsJsonToRes(msg).data; this.state.crossPosts = data.posts.filter( p => p.post.id != Number(this.props.match.params.id) ); this.setState(this.state); } else if (op == UserOperation.LeaveAdmin) { let data = wsJsonToRes(msg).data; this.state.siteRes = data; this.setState(this.state); } else if (op == UserOperation.TransferCommunity) { let data = wsJsonToRes(msg).data; this.state.postRes.community_view = data.community_view; this.state.postRes.post_view.community = data.community_view.community; this.state.postRes.moderators = data.moderators; this.setState(this.state); } else if (op == UserOperation.BlockPerson) { let data = wsJsonToRes(msg).data; updatePersonBlock(data); } else if (op == UserOperation.CreatePostReport) { let data = wsJsonToRes(msg).data; if (data) { toast(i18n.t("report_created")); } } else if (op == UserOperation.CreateCommentReport) { let data = wsJsonToRes(msg).data; if (data) { toast(i18n.t("report_created")); } } } }