import { Component, linkEvent } from "inferno"; import { Link } from "inferno-router"; import { AddAdmin, AddModToCommunity, BanFromCommunity, BanPerson, BlockPerson, CommunityModeratorView, CreatePostLike, CreatePostReport, DeletePost, LockPost, PersonViewSafe, PostView, RemovePost, SavePost, StickyPost, TransferCommunity, TransferSite, } from "lemmy-js-client"; import { externalHost } from "../../env"; import { i18n } from "../../i18next"; import { BanType } from "../../interfaces"; import { UserService, WebSocketService } from "../../services"; import { authField, canMod, getUnixTime, hostname, isImage, isMod, isVideo, md, mdToHtml, numToSI, previewLines, setupTippy, showScores, wsClient, } from "../../utils"; import { Icon } from "../common/icon"; import { MomentTime } from "../common/moment-time"; import { PictrsImage } from "../common/pictrs-image"; import { CommunityLink } from "../community/community-link"; import { PersonListing } from "../person/person-listing"; import { MetadataCard } from "./metadata-card"; import { PostForm } from "./post-form"; interface PostListingState { showEdit: boolean; showRemoveDialog: boolean; removeReason: string; showBanDialog: boolean; removeData: boolean; banReason: string; banExpires: string; banType: BanType; showConfirmTransferSite: boolean; showConfirmTransferCommunity: boolean; imageExpanded: boolean; viewSource: boolean; showAdvanced: boolean; showMoreMobile: boolean; showBody: boolean; showReportDialog: boolean; reportReason: string; my_vote: number; score: number; upvotes: number; downvotes: number; } interface PostListingProps { post_view: PostView; duplicates?: PostView[]; showCommunity?: boolean; showBody?: boolean; moderators?: CommunityModeratorView[]; admins?: PersonViewSafe[]; enableDownvotes: boolean; enableNsfw: boolean; } export class PostListing extends Component { private emptyState: PostListingState = { showEdit: false, showRemoveDialog: false, removeReason: null, showBanDialog: false, removeData: false, banReason: null, banExpires: null, banType: BanType.Community, showConfirmTransferSite: false, showConfirmTransferCommunity: false, imageExpanded: false, viewSource: false, showAdvanced: false, showMoreMobile: false, showBody: false, showReportDialog: false, reportReason: null, my_vote: this.props.post_view.my_vote, score: this.props.post_view.counts.score, upvotes: this.props.post_view.counts.upvotes, downvotes: this.props.post_view.counts.downvotes, }; constructor(props: any, context: any) { super(props, context); this.state = this.emptyState; this.handlePostLike = this.handlePostLike.bind(this); this.handlePostDisLike = this.handlePostDisLike.bind(this); this.handleEditPost = this.handleEditPost.bind(this); this.handleEditCancel = this.handleEditCancel.bind(this); } componentWillReceiveProps(nextProps: PostListingProps) { this.state.my_vote = nextProps.post_view.my_vote; this.state.upvotes = nextProps.post_view.counts.upvotes; this.state.downvotes = nextProps.post_view.counts.downvotes; this.state.score = nextProps.post_view.counts.score; if (this.props.post_view.post.id !== nextProps.post_view.post.id) { this.state.imageExpanded = false; } this.setState(this.state); } render() { return (
{!this.state.showEdit ? ( <> {this.listing()} {this.body()} ) : (
)}
); } body() { let post = this.props.post_view.post; return (
{post.url && this.showBody && post.embed_title && ( )} {this.showBody && post.body && (this.state.viewSource ? (
{post.body}
) : (
))}
); } imgThumb(src: string) { let post_view = this.props.post_view; return ( ); } getImageSrc(): string { let post = this.props.post_view.post; if (isImage(post.url)) { if (post.url.includes("pictrs")) { return post.url; } else if (post.thumbnail_url) { return post.thumbnail_url; } else { return post.url; } } else if (post.thumbnail_url) { return post.thumbnail_url; } else { return null; } } thumbnail() { let post = this.props.post_view.post; if (isImage(post.url)) { return ( {this.imgThumb(this.getImageSrc())} ); } else if (post.thumbnail_url) { return ( {this.imgThumb(this.getImageSrc())} ); } else if (post.url) { if (isVideo(post.url)) { return (
); } else { return (
); } } else { return (
); } } createdLine() { let post_view = this.props.post_view; return (
  • {this.isMod && ( {i18n.t("mod")} )} {this.isAdmin && ( {i18n.t("admin")} )} {(post_view.creator_banned_from_community || post_view.creator.banned) && ( {i18n.t("banned")} )} {post_view.creator_blocked && ( {"blocked"} )} {this.props.showCommunity && ( {i18n.t("to")} )}
  • {post_view.post.url && !(hostname(post_view.post.url) == externalHost) && ( <>
  • {hostname(post_view.post.url)}
  • )}
  • {post_view.post.body && ( <>
  • )}
); } voteBar() { return (
{showScores() ? (
{numToSI(this.state.score)}
) : (
)} {this.props.enableDownvotes && ( )}
); } postTitleLine() { let post = this.props.post_view.post; return (
{this.showBody && post.url ? ( {post.name} ) : ( {post.name} )} {(isImage(post.url) || post.thumbnail_url) && (!this.state.imageExpanded ? ( ) : ( ))} {post.removed && ( {i18n.t("removed")} )} {post.deleted && ( )} {post.locked && ( )} {post.stickied && ( )} {post.nsfw && ( {i18n.t("nsfw")} )}
); } commentsLine(mobile = false) { let post_view = this.props.post_view; return (
{!mobile && ( <> {this.state.downvotes !== 0 && showScores() && ( )} {!this.showBody && ( )} )} {/* This is an expanding spacer for mobile */}
{mobile && ( <>
{showScores() ? ( ) : ( )} {this.props.enableDownvotes && (showScores() ? ( ) : ( ))}
{!this.state.showMoreMobile && this.showBody && ( )} {this.state.showMoreMobile && this.postActions(mobile)} )}
); } duplicatesLine() { let dupes = this.props.duplicates; return ( dupes && dupes.length > 0 && (
    <>
  • {i18n.t("cross_posted_to")}
  • {dupes.map(pv => (
  • {pv.community.local ? pv.community.name : `${pv.community.name}@${hostname(pv.community.actor_id)}`}
  • ))}
) ); } postActions(mobile = false) { let post_view = this.props.post_view; return ( UserService.Instance.myUserInfo && ( <> {this.showBody && ( <> {!mobile && ( )} {!this.myPost && ( <> )} )} {this.myPost && this.showBody && ( <> )} {!this.state.showAdvanced && this.showBody ? ( ) : ( <> {this.showBody && post_view.post.body && ( )} {this.canModOnSelf && ( <> )} {/* Mods can ban from community, and appoint as mods to community */} {(this.canMod || this.canAdmin) && (!post_view.post.removed ? ( ) : ( ))} {this.canMod && ( <> {!this.isMod && (!post_view.creator_banned_from_community ? ( ) : ( ))} {!post_view.creator_banned_from_community && ( )} )} {/* Community creators and admins can transfer community to another mod */} {(this.amCommunityCreator || this.canAdmin) && this.isMod && (!this.state.showConfirmTransferCommunity ? ( ) : ( <> ))} {/* Admins can ban from all, and appoint other admins */} {this.canAdmin && ( <> {!this.isAdmin && (!post_view.creator.banned ? ( ) : ( ))} {!post_view.creator.banned && post_view.creator.local && ( )} )} {/* Site Creator can transfer to another admin */} {this.amSiteCreator && this.isAdmin && (!this.state.showConfirmTransferSite ? ( ) : ( <> ))} )} ) ); } removeAndBanDialogs() { let post = this.props.post_view; return ( <> {this.state.showRemoveDialog && (
)} {this.state.showBanDialog && (
{/* TODO hold off on expires until later */} {/*
*/} {/* */} {/* */} {/*
*/}
)} {this.state.showReportDialog && (
)} ); } mobileThumbnail() { let post = this.props.post_view.post; return post.thumbnail_url || isImage(post.url) ? (
{this.postTitleLine()}
{/* Post body prev or thumbnail */} {!this.state.imageExpanded && this.thumbnail()}
) : ( this.postTitleLine() ); } showMobilePreview() { let post = this.props.post_view.post; return ( post.body && !this.showBody && (
) ); } listing() { return ( <> {/* The mobile view*/}
{this.createdLine()} {/* If it has a thumbnail, do a right aligned thumbnail */} {this.mobileThumbnail()} {/* Show a preview of the post body */} {this.showMobilePreview()} {this.commentsLine(true)} {this.duplicatesLine()} {this.removeAndBanDialogs()}
{/* The larger view*/}
{this.voteBar()} {!this.state.imageExpanded && (
{this.thumbnail()}
)}
{this.postTitleLine()} {this.createdLine()} {this.commentsLine()} {this.duplicatesLine()} {this.postActions()} {this.removeAndBanDialogs()}
); } private get myPost(): boolean { return ( UserService.Instance.myUserInfo && this.props.post_view.creator.id == UserService.Instance.myUserInfo.local_user_view.person.id ); } get isMod(): boolean { return ( this.props.moderators && isMod( this.props.moderators.map(m => m.moderator.id), this.props.post_view.creator.id ) ); } get isAdmin(): boolean { return ( this.props.admins && isMod( this.props.admins.map(a => a.person.id), this.props.post_view.creator.id ) ); } get canMod(): boolean { if (this.props.admins && this.props.moderators) { let adminsThenMods = this.props.admins .map(a => a.person.id) .concat(this.props.moderators.map(m => m.moderator.id)); return canMod( UserService.Instance.myUserInfo, adminsThenMods, this.props.post_view.creator.id ); } else { return false; } } get canModOnSelf(): boolean { if (this.props.admins && this.props.moderators) { let adminsThenMods = this.props.admins .map(a => a.person.id) .concat(this.props.moderators.map(m => m.moderator.id)); return canMod( UserService.Instance.myUserInfo, adminsThenMods, this.props.post_view.creator.id, true ); } else { return false; } } get canAdmin(): boolean { return ( this.props.admins && canMod( UserService.Instance.myUserInfo, this.props.admins.map(a => a.person.id), this.props.post_view.creator.id ) ); } get amCommunityCreator(): boolean { return ( this.props.moderators && UserService.Instance.myUserInfo && this.props.post_view.creator.id != UserService.Instance.myUserInfo.local_user_view.person.id && UserService.Instance.myUserInfo.local_user_view.person.id == this.props.moderators[0].moderator.id ); } get amSiteCreator(): boolean { return ( this.props.admins && UserService.Instance.myUserInfo && this.props.post_view.creator.id != UserService.Instance.myUserInfo.local_user_view.person.id && UserService.Instance.myUserInfo.local_user_view.person.id == this.props.admins[0].person.id ); } handlePostLike(i: PostListing, event: any) { event.preventDefault(); if (!UserService.Instance.myUserInfo) { this.context.router.history.push(`/login`); } let new_vote = i.state.my_vote == 1 ? 0 : 1; if (i.state.my_vote == 1) { i.state.score--; i.state.upvotes--; } else if (i.state.my_vote == -1) { i.state.downvotes--; i.state.upvotes++; i.state.score += 2; } else { i.state.upvotes++; i.state.score++; } i.state.my_vote = new_vote; let form: CreatePostLike = { post_id: i.props.post_view.post.id, score: i.state.my_vote, auth: authField(), }; WebSocketService.Instance.send(wsClient.likePost(form)); i.setState(i.state); setupTippy(); } handlePostDisLike(i: PostListing, event: any) { event.preventDefault(); if (!UserService.Instance.myUserInfo) { this.context.router.history.push(`/login`); } let new_vote = i.state.my_vote == -1 ? 0 : -1; if (i.state.my_vote == 1) { i.state.score -= 2; i.state.upvotes--; i.state.downvotes++; } else if (i.state.my_vote == -1) { i.state.downvotes--; i.state.score++; } else { i.state.downvotes++; i.state.score--; } i.state.my_vote = new_vote; let form: CreatePostLike = { post_id: i.props.post_view.post.id, score: i.state.my_vote, auth: authField(), }; WebSocketService.Instance.send(wsClient.likePost(form)); i.setState(i.state); setupTippy(); } handleEditClick(i: PostListing) { i.state.showEdit = true; i.setState(i.state); } handleEditCancel() { this.state.showEdit = false; this.setState(this.state); } // The actual editing is done in the recieve for post handleEditPost() { this.state.showEdit = false; this.setState(this.state); } handleShowReportDialog(i: PostListing) { i.state.showReportDialog = !i.state.showReportDialog; i.setState(this.state); } handleReportReasonChange(i: PostListing, event: any) { i.state.reportReason = event.target.value; i.setState(i.state); } handleReportSubmit(i: PostListing, event: any) { event.preventDefault(); let form: CreatePostReport = { post_id: i.props.post_view.post.id, reason: i.state.reportReason, auth: authField(), }; WebSocketService.Instance.send(wsClient.createPostReport(form)); i.state.showReportDialog = false; i.setState(i.state); } handleBlockUserClick(i: PostListing) { let blockUserForm: BlockPerson = { person_id: i.props.post_view.creator.id, block: true, auth: authField(), }; WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); } handleDeleteClick(i: PostListing) { let deleteForm: DeletePost = { post_id: i.props.post_view.post.id, deleted: !i.props.post_view.post.deleted, auth: authField(), }; WebSocketService.Instance.send(wsClient.deletePost(deleteForm)); } handleSavePostClick(i: PostListing) { let saved = i.props.post_view.saved == undefined ? true : !i.props.post_view.saved; let form: SavePost = { post_id: i.props.post_view.post.id, save: saved, auth: authField(), }; WebSocketService.Instance.send(wsClient.savePost(form)); } get crossPostParams(): string { let post = this.props.post_view.post; let params = `?title=${encodeURIComponent(post.name)}`; if (post.url) { params += `&url=${encodeURIComponent(post.url)}`; } if (post.body) { params += `&body=${encodeURIComponent(this.crossPostBody())}`; } return params; } crossPostBody(): string { let post = this.props.post_view.post; let body = `${i18n.t("cross_posted_from")} ${ post.ap_id }\n\n${post.body.replace(/^/gm, "> ")}`; return body; } get showBody(): boolean { return this.props.showBody || this.state.showBody; } handleModRemoveShow(i: PostListing) { i.state.showRemoveDialog = !i.state.showRemoveDialog; i.state.showBanDialog = false; i.setState(i.state); } handleModRemoveReasonChange(i: PostListing, event: any) { i.state.removeReason = event.target.value; i.setState(i.state); } handleModRemoveDataChange(i: PostListing, event: any) { i.state.removeData = event.target.checked; i.setState(i.state); } handleModRemoveSubmit(i: PostListing, event: any) { event.preventDefault(); let form: RemovePost = { post_id: i.props.post_view.post.id, removed: !i.props.post_view.post.removed, reason: i.state.removeReason, auth: authField(), }; WebSocketService.Instance.send(wsClient.removePost(form)); i.state.showRemoveDialog = false; i.setState(i.state); } handleModLock(i: PostListing) { let form: LockPost = { post_id: i.props.post_view.post.id, locked: !i.props.post_view.post.locked, auth: authField(), }; WebSocketService.Instance.send(wsClient.lockPost(form)); } handleModSticky(i: PostListing) { let form: StickyPost = { post_id: i.props.post_view.post.id, stickied: !i.props.post_view.post.stickied, auth: authField(), }; WebSocketService.Instance.send(wsClient.stickyPost(form)); } handleModBanFromCommunityShow(i: PostListing) { i.state.showBanDialog = true; i.state.banType = BanType.Community; i.state.showRemoveDialog = false; i.setState(i.state); } handleModBanShow(i: PostListing) { i.state.showBanDialog = true; i.state.banType = BanType.Site; i.state.showRemoveDialog = false; i.setState(i.state); } handleModBanReasonChange(i: PostListing, event: any) { i.state.banReason = event.target.value; i.setState(i.state); } handleModBanExpiresChange(i: PostListing, event: any) { i.state.banExpires = event.target.value; i.setState(i.state); } handleModBanFromCommunitySubmit(i: PostListing) { i.state.banType = BanType.Community; i.setState(i.state); i.handleModBanBothSubmit(i); } handleModBanSubmit(i: PostListing) { i.state.banType = BanType.Site; i.setState(i.state); i.handleModBanBothSubmit(i); } handleModBanBothSubmit(i: PostListing, event?: any) { if (event) event.preventDefault(); if (i.state.banType == BanType.Community) { // If its an unban, restore all their data let ban = !i.props.post_view.creator_banned_from_community; if (ban == false) { i.state.removeData = false; } let form: BanFromCommunity = { person_id: i.props.post_view.creator.id, community_id: i.props.post_view.community.id, ban, remove_data: i.state.removeData, reason: i.state.banReason, expires: getUnixTime(i.state.banExpires), auth: authField(), }; WebSocketService.Instance.send(wsClient.banFromCommunity(form)); } else { // If its an unban, restore all their data let ban = !i.props.post_view.creator.banned; if (ban == false) { i.state.removeData = false; } let form: BanPerson = { person_id: i.props.post_view.creator.id, ban, remove_data: i.state.removeData, reason: i.state.banReason, expires: getUnixTime(i.state.banExpires), auth: authField(), }; WebSocketService.Instance.send(wsClient.banPerson(form)); } i.state.showBanDialog = false; i.setState(i.state); } handleAddModToCommunity(i: PostListing) { let form: AddModToCommunity = { person_id: i.props.post_view.creator.id, community_id: i.props.post_view.community.id, added: !i.isMod, auth: authField(), }; WebSocketService.Instance.send(wsClient.addModToCommunity(form)); i.setState(i.state); } handleAddAdmin(i: PostListing) { let form: AddAdmin = { person_id: i.props.post_view.creator.id, added: !i.isAdmin, auth: authField(), }; WebSocketService.Instance.send(wsClient.addAdmin(form)); i.setState(i.state); } handleShowConfirmTransferCommunity(i: PostListing) { i.state.showConfirmTransferCommunity = true; i.setState(i.state); } handleCancelShowConfirmTransferCommunity(i: PostListing) { i.state.showConfirmTransferCommunity = false; i.setState(i.state); } handleTransferCommunity(i: PostListing) { let form: TransferCommunity = { community_id: i.props.post_view.community.id, person_id: i.props.post_view.creator.id, auth: authField(), }; WebSocketService.Instance.send(wsClient.transferCommunity(form)); i.state.showConfirmTransferCommunity = false; i.setState(i.state); } handleShowConfirmTransferSite(i: PostListing) { i.state.showConfirmTransferSite = true; i.setState(i.state); } handleCancelShowConfirmTransferSite(i: PostListing) { i.state.showConfirmTransferSite = false; i.setState(i.state); } handleTransferSite(i: PostListing) { let form: TransferSite = { person_id: i.props.post_view.creator.id, auth: authField(), }; WebSocketService.Instance.send(wsClient.transferSite(form)); i.state.showConfirmTransferSite = false; i.setState(i.state); } handleImageExpandClick(i: PostListing, event: any) { event.preventDefault(); i.state.imageExpanded = !i.state.imageExpanded; i.setState(i.state); } handleViewSource(i: PostListing) { i.state.viewSource = !i.state.viewSource; i.setState(i.state); } handleShowAdvanced(i: PostListing) { i.state.showAdvanced = !i.state.showAdvanced; i.setState(i.state); setupTippy(); } handleShowMoreMobile(i: PostListing) { i.state.showMoreMobile = !i.state.showMoreMobile; i.state.showAdvanced = !i.state.showAdvanced; i.setState(i.state); setupTippy(); } handleShowBody(i: PostListing) { i.state.showBody = !i.state.showBody; i.setState(i.state); setupTippy(); } get pointsTippy(): string { let points = i18n.t("number_of_points", { count: this.state.score, formattedCount: this.state.score, }); let upvotes = i18n.t("number_of_upvotes", { count: this.state.upvotes, formattedCount: this.state.upvotes, }); let downvotes = i18n.t("number_of_downvotes", { count: this.state.downvotes, formattedCount: this.state.downvotes, }); return `${points} • ${upvotes} • ${downvotes}`; } }