import classNames from "classnames"; import { Component, linkEvent } from "inferno"; import { Link } from "inferno-router"; import { AddAdmin, AddModToCommunity, BanFromCommunity, BanPerson, BlockPerson, CommunityModeratorView, CreatePostLike, CreatePostReport, DeletePost, EditPost, FeaturePost, Language, LockPost, PersonView, PostView, PurgePerson, PurgePost, RemovePost, SavePost, TransferCommunity, } from "lemmy-js-client"; import { getExternalHost, getHttpBase } from "../../env"; import { i18n } from "../../i18next"; import { BanType, PostFormParams, PurgeType, VoteType } from "../../interfaces"; import { UserService } from "../../services"; import { amAdmin, amCommunityCreator, amMod, canAdmin, canMod, canShare, futureDaysToUnixTime, hostname, isAdmin, isBanned, isImage, isMod, isVideo, mdNoImages, mdToHtml, mdToHtmlInline, myAuthRequired, newVote, numToSI, relTags, setupTippy, share, showScores, } from "../../utils"; import { Icon, PurgeWarning, Spinner } 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; showPurgeDialog: boolean; purgeReason?: string; purgeType?: PurgeType; purgeLoading: boolean; removeReason?: string; showBanDialog: boolean; banReason?: string; banExpireDays?: number; banType?: BanType; removeData?: boolean; showConfirmTransferSite: boolean; showConfirmTransferCommunity: boolean; imageExpanded: boolean; viewSource: boolean; showAdvanced: boolean; showMoreMobile: boolean; showBody: boolean; showReportDialog: boolean; reportReason?: string; upvoteLoading: boolean; downvoteLoading: boolean; reportLoading: boolean; blockLoading: boolean; lockLoading: boolean; deleteLoading: boolean; removeLoading: boolean; saveLoading: boolean; featureCommunityLoading: boolean; featureLocalLoading: boolean; banLoading: boolean; addModLoading: boolean; addAdminLoading: boolean; transferLoading: boolean; } interface PostListingProps { post_view: PostView; crossPosts?: PostView[]; moderators?: CommunityModeratorView[]; admins?: PersonView[]; allLanguages: Language[]; siteLanguages: number[]; showCommunity?: boolean; showBody?: boolean; hideImage?: boolean; enableDownvotes?: boolean; enableNsfw?: boolean; viewOnly?: boolean; onPostEdit(form: EditPost): void; onPostVote(form: CreatePostLike): void; onPostReport(form: CreatePostReport): void; onBlockPerson(form: BlockPerson): void; onLockPost(form: LockPost): void; onDeletePost(form: DeletePost): void; onRemovePost(form: RemovePost): void; onSavePost(form: SavePost): void; onFeaturePost(form: FeaturePost): void; onPurgePerson(form: PurgePerson): void; onPurgePost(form: PurgePost): void; onBanPersonFromCommunity(form: BanFromCommunity): void; onBanPerson(form: BanPerson): void; onAddModToCommunity(form: AddModToCommunity): void; onAddAdmin(form: AddAdmin): void; onTransferCommunity(form: TransferCommunity): void; } export class PostListing extends Component { state: PostListingState = { showEdit: false, showRemoveDialog: false, showPurgeDialog: false, purgeType: PurgeType.Person, showBanDialog: false, banType: BanType.Community, removeData: false, showConfirmTransferSite: false, showConfirmTransferCommunity: false, imageExpanded: false, viewSource: false, showAdvanced: false, showMoreMobile: false, showBody: false, showReportDialog: false, upvoteLoading: false, downvoteLoading: false, purgeLoading: false, reportLoading: false, blockLoading: false, lockLoading: false, deleteLoading: false, removeLoading: false, saveLoading: false, featureCommunityLoading: false, featureLocalLoading: false, banLoading: false, addModLoading: false, addAdminLoading: false, transferLoading: false, }; constructor(props: any, context: any) { super(props, context); this.handleEditPost = this.handleEditPost.bind(this); this.handleEditCancel = this.handleEditCancel.bind(this); } componentWillReceiveProps(nextProps: PostListingProps) { if (this.props !== nextProps) { this.setState({ upvoteLoading: false, downvoteLoading: false, purgeLoading: false, reportLoading: false, blockLoading: false, lockLoading: false, deleteLoading: false, removeLoading: false, saveLoading: false, featureCommunityLoading: false, featureLocalLoading: false, banLoading: false, addModLoading: false, addAdminLoading: false, transferLoading: false, imageExpanded: false, }); } } get postView(): PostView { return this.props.post_view; } render() { const post = this.postView.post; return (
{!this.state.showEdit ? ( <> {this.listing()} {this.state.imageExpanded && !this.props.hideImage && this.img} {post.url && this.state.showBody && post.embed_title && ( )} {this.showBody && this.body()} ) : ( )}
); } body() { const body = this.postView.post.body; return body ? (
{this.state.viewSource ? (
{body}
) : (
)}
) : ( <> ); } get img() { return this.imageSrc ? ( <>
) : ( <> ); } imgThumb(src: string) { const post_view = this.postView; return ( ); } get imageSrc(): string | undefined { const post = this.postView.post; const url = post.url; const thumbnail = post.thumbnail_url; if (url && isImage(url)) { if (url.includes("pictrs")) { return url; } else if (thumbnail) { return thumbnail; } else { return url; } } else if (thumbnail) { return thumbnail; } else { return undefined; } } thumbnail() { const post = this.postView.post; const url = post.url; const thumbnail = post.thumbnail_url; if (!this.props.hideImage && url && isImage(url) && this.imageSrc) { return ( {this.imgThumb(this.imageSrc)} ); } else if (!this.props.hideImage && url && thumbnail && this.imageSrc) { return ( {this.imgThumb(this.imageSrc)} ); } else if (url) { if (!this.props.hideImage && isVideo(url)) { return (
); } else { return (
); } } else { return (
); } } createdLine() { const post_view = this.postView; const url = post_view.post.url; const body = post_view.post.body; return ( ); } voteBar() { return (
{showScores() ? (
{numToSI(this.postView.counts.score)}
) : (
)} {this.props.enableDownvotes && ( )}
); } get postLink() { const post = this.postView.post; return (
); } postTitleLine() { const post = this.postView.post; const url = post.url; return (
{url ? ( this.props.showBody ? (
) : ( this.postLink ) ) : ( this.postLink )} {(url && isImage(url)) || (post.thumbnail_url && ( ))} {post.removed && ( {i18n.t("removed")} )} {post.deleted && ( )} {post.locked && ( )} {post.featured_community && ( )} {post.featured_local && ( )} {post.nsfw && ( {i18n.t("nsfw")} )}
); } duplicatesLine() { const dupes = this.props.crossPosts; 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)}`}
  • ))}
) : ( <> ); } commentsLine(mobile = false) { const post = this.postView.post; return (
{this.commentsButton} {canShare() && ( )} {!post.local && ( )} {mobile && !this.props.viewOnly && this.mobileVotes} {UserService.Instance.myUserInfo && !this.props.viewOnly && this.postActions()}
); } get hasAdvancedButtons() { return ( this.myPost || (this.showBody && this.postView.post.body) || amMod(this.props.moderators) || amAdmin() || this.canMod_ || this.canAdmin_ ); } postActions() { // Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button. // Possible enhancement: Make each button a component. const post_view = this.postView; return ( <> {this.saveButton} {this.crossPostButton} {this.showBody && post_view.post.body && this.viewSourceButton} {this.hasAdvancedButtons && (
    {!this.myPost ? ( <>
  • {this.reportButton}
  • {this.blockButton}
  • ) : ( <>
  • {this.editButton}
  • {this.deleteButton}
  • )} {/* Any mod can do these, not limited to hierarchy*/} {(amMod(this.props.moderators) || amAdmin()) && ( <>

  • {this.lockButton}
  • {this.featureButtons} )} {(this.canMod_ || this.canAdmin_) && (
  • {this.modRemoveButton}
  • )}
)} ); } get commentsButton() { const post_view = this.postView; return ( {i18n.t("number_of_comments", { count: Number(post_view.counts.comments), formattedCount: numToSI(post_view.counts.comments), })} {this.unreadCount && ( ({this.unreadCount} {i18n.t("new")}) )} ); } get unreadCount(): number | undefined { const pv = this.postView; return pv.unread_comments == pv.counts.comments || pv.unread_comments == 0 ? undefined : pv.unread_comments; } get mobileVotes() { // TODO: make nicer const tippy = showScores() ? { "data-tippy-content": this.pointsTippy } : {}; return ( <>
{this.props.enableDownvotes && ( )}
); } get saveButton() { const saved = this.postView.saved; const label = saved ? i18n.t("unsave") : i18n.t("save"); return ( ); } get crossPostButton() { return ( ); } get reportButton() { return ( ); } get blockButton() { return ( ); } get editButton() { return ( ); } get deleteButton() { const deleted = this.postView.post.deleted; const label = !deleted ? i18n.t("delete") : i18n.t("restore"); return ( ); } get viewSourceButton() { return ( ); } get lockButton() { const locked = this.postView.post.locked; const label = locked ? i18n.t("unlock") : i18n.t("lock"); return ( ); } get featureButtons() { const featuredCommunity = this.postView.post.featured_community; const labelCommunity = featuredCommunity ? i18n.t("unfeature_from_community") : i18n.t("feature_in_community"); const featuredLocal = this.postView.post.featured_local; const labelLocal = featuredLocal ? i18n.t("unfeature_from_local") : i18n.t("feature_in_local"); return ( <>
  • {amAdmin() && ( )}
  • ); } get modRemoveButton() { const removed = this.postView.post.removed; return ( ); } /** * Mod/Admin actions to be taken against the author. */ userActionsLine() { // TODO: make nicer const post_view = this.postView; return ( this.state.showAdvanced && ( <> {this.canMod_ && ( <> {!this.creatorIsMod_ && (!post_view.creator_banned_from_community ? ( ) : ( ))} {!post_view.creator_banned_from_community && ( )} )} {/* Community creators and admins can transfer community to another mod */} {(amCommunityCreator(post_view.creator.id, this.props.moderators) || this.canAdmin_) && this.creatorIsMod_ && (!this.state.showConfirmTransferCommunity ? ( ) : ( <> ))} {/* Admins can ban from all, and appoint other admins */} {this.canAdmin_ && ( <> {!this.creatorIsAdmin_ && ( <> {!isBanned(post_view.creator) ? ( ) : ( )} )} {!isBanned(post_view.creator) && post_view.creator.local && ( )} )} ) ); } removeAndBanDialogs() { const post = this.postView; const purgeTypeText = this.state.purgeType == PurgeType.Post ? i18n.t("purge_post") : `${i18n.t("purge")} ${post.creator.name}`; return ( <> {this.state.showRemoveDialog && (
    )} {this.state.showBanDialog && (
    {/* TODO hold off on expires until later */} {/*
    */} {/* */} {/* */} {/*
    */}
    )} {this.state.showReportDialog && (
    )} {this.state.showPurgeDialog && (
    {this.state.purgeLoading ? ( ) : ( )} )} ); } mobileThumbnail() { const post = this.postView.post; return post.thumbnail_url || (post.url && isImage(post.url)) ? (
    {this.postTitleLine()}
    {/* Post body prev or thumbnail */} {!this.state.imageExpanded && this.thumbnail()}
    ) : ( this.postTitleLine() ); } showMobilePreview() { const { body, id } = this.postView.post; return !this.showBody && body ? (
    {body}
    ) : ( <> ); } 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.userActionsLine()} {this.duplicatesLine()} {this.removeAndBanDialogs()}
    {/* The larger view*/}
    {!this.props.viewOnly && this.voteBar()}
    {this.thumbnail()}
    {this.postTitleLine()} {this.createdLine()} {this.commentsLine()} {this.duplicatesLine()} {this.userActionsLine()} {this.removeAndBanDialogs()}
    ); } private get myPost(): boolean { return ( this.postView.creator.id == UserService.Instance.myUserInfo?.local_user_view.person.id ); } handleEditClick(i: PostListing) { i.setState({ showEdit: true }); } handleEditCancel() { this.setState({ showEdit: false }); } // The actual editing is done in the receive for post handleEditPost(form: EditPost) { this.setState({ showEdit: false }); this.props.onPostEdit(form); } handleShare(i: PostListing) { const { name, body, id } = i.props.post_view.post; share({ title: name, text: body?.slice(0, 50), url: `${getHttpBase()}/post/${id}`, }); } handleShowReportDialog(i: PostListing) { i.setState({ showReportDialog: !i.state.showReportDialog }); } handleReportReasonChange(i: PostListing, event: any) { i.setState({ reportReason: event.target.value }); } handleReportSubmit(i: PostListing, event: any) { event.preventDefault(); i.setState({ reportLoading: true }); i.props.onPostReport({ post_id: i.postView.post.id, reason: i.state.reportReason ?? "", auth: myAuthRequired(), }); } handleBlockPersonClick(i: PostListing) { i.setState({ blockLoading: true }); i.props.onBlockPerson({ person_id: i.postView.creator.id, block: true, auth: myAuthRequired(), }); } handleDeleteClick(i: PostListing) { i.setState({ deleteLoading: true }); i.props.onDeletePost({ post_id: i.postView.post.id, deleted: !i.postView.post.deleted, auth: myAuthRequired(), }); } handleSavePostClick(i: PostListing) { i.setState({ saveLoading: true }); i.props.onSavePost({ post_id: i.postView.post.id, save: !i.postView.saved, auth: myAuthRequired(), }); } get crossPostParams(): PostFormParams { const queryParams: PostFormParams = {}; const { name, url } = this.postView.post; queryParams.name = name; if (url) { queryParams.url = url; } const crossPostBody = this.crossPostBody(); if (crossPostBody) { queryParams.body = crossPostBody; } return queryParams; } crossPostBody(): string | undefined { const post = this.postView.post; const body = post.body; return body ? `${i18n.t("cross_posted_from")} ${post.ap_id}\n\n${body.replace( /^/gm, "> " )}` : undefined; } get showBody(): boolean { return this.props.showBody || this.state.showBody; } handleModRemoveShow(i: PostListing) { i.setState({ showRemoveDialog: !i.state.showRemoveDialog, showBanDialog: false, }); } handleModRemoveReasonChange(i: PostListing, event: any) { i.setState({ removeReason: event.target.value }); } handleModRemoveDataChange(i: PostListing, event: any) { i.setState({ removeData: event.target.checked }); } handleModRemoveSubmit(i: PostListing, event: any) { event.preventDefault(); i.setState({ removeLoading: true }); i.props.onRemovePost({ post_id: i.postView.post.id, removed: !i.postView.post.removed, auth: myAuthRequired(), }); } handleModLock(i: PostListing) { i.setState({ lockLoading: true }); i.props.onLockPost({ post_id: i.postView.post.id, locked: !i.postView.post.locked, auth: myAuthRequired(), }); } handleModFeaturePostLocal(i: PostListing) { i.setState({ featureLocalLoading: true }); i.props.onFeaturePost({ post_id: i.postView.post.id, featured: !i.postView.post.featured_local, feature_type: "Local", auth: myAuthRequired(), }); } handleModFeaturePostCommunity(i: PostListing) { i.setState({ featureCommunityLoading: true }); i.props.onFeaturePost({ post_id: i.postView.post.id, featured: !i.postView.post.featured_community, feature_type: "Community", auth: myAuthRequired(), }); } handleModBanFromCommunityShow(i: PostListing) { i.setState({ showBanDialog: true, banType: BanType.Community, showRemoveDialog: false, }); } handleModBanShow(i: PostListing) { i.setState({ showBanDialog: true, banType: BanType.Site, showRemoveDialog: false, }); } handlePurgePersonShow(i: PostListing) { i.setState({ showPurgeDialog: true, purgeType: PurgeType.Person, showRemoveDialog: false, }); } handlePurgePostShow(i: PostListing) { i.setState({ showPurgeDialog: true, purgeType: PurgeType.Post, showRemoveDialog: false, }); } handlePurgeReasonChange(i: PostListing, event: any) { i.setState({ purgeReason: event.target.value }); } handlePurgeSubmit(i: PostListing, event: any) { event.preventDefault(); i.setState({ purgeLoading: true }); if (i.state.purgeType == PurgeType.Person) { i.props.onPurgePerson({ person_id: i.postView.creator.id, reason: i.state.purgeReason, auth: myAuthRequired(), }); } else if (i.state.purgeType == PurgeType.Post) { i.props.onPurgePost({ post_id: i.postView.post.id, reason: i.state.purgeReason, auth: myAuthRequired(), }); } } handleModBanReasonChange(i: PostListing, event: any) { i.setState({ banReason: event.target.value }); } handleModBanExpireDaysChange(i: PostListing, event: any) { i.setState({ banExpireDays: event.target.value }); } handleModBanFromCommunitySubmit(i: PostListing, event: any) { i.setState({ banType: BanType.Community }); i.handleModBanBothSubmit(i, event); } handleModBanSubmit(i: PostListing, event: any) { i.setState({ banType: BanType.Site }); i.handleModBanBothSubmit(i, event); } handleModBanBothSubmit(i: PostListing, event: any) { event.preventDefault(); i.setState({ banLoading: true }); const ban = !i.props.post_view.creator_banned_from_community; // If its an unban, restore all their data if (ban == false) { i.setState({ removeData: false }); } const person_id = i.props.post_view.creator.id; const remove_data = i.state.removeData; const reason = i.state.banReason; const expires = futureDaysToUnixTime(i.state.banExpireDays); if (i.state.banType == BanType.Community) { const community_id = i.postView.community.id; i.props.onBanPersonFromCommunity({ community_id, person_id, ban, remove_data, reason, expires, auth: myAuthRequired(), }); } else { i.props.onBanPerson({ person_id, ban, remove_data, reason, expires, auth: myAuthRequired(), }); } } handleAddModToCommunity(i: PostListing) { i.setState({ addModLoading: true }); i.props.onAddModToCommunity({ community_id: i.postView.community.id, person_id: i.postView.creator.id, added: !i.creatorIsMod_, auth: myAuthRequired(), }); } handleAddAdmin(i: PostListing) { i.setState({ addAdminLoading: true }); i.props.onAddAdmin({ person_id: i.postView.creator.id, added: !i.creatorIsAdmin_, auth: myAuthRequired(), }); } handleShowConfirmTransferCommunity(i: PostListing) { i.setState({ showConfirmTransferCommunity: true }); } handleCancelShowConfirmTransferCommunity(i: PostListing) { i.setState({ showConfirmTransferCommunity: false }); } handleTransferCommunity(i: PostListing) { i.setState({ transferLoading: true }); i.props.onTransferCommunity({ community_id: i.postView.community.id, person_id: i.postView.creator.id, auth: myAuthRequired(), }); } handleShowConfirmTransferSite(i: PostListing) { i.setState({ showConfirmTransferSite: true }); } handleCancelShowConfirmTransferSite(i: PostListing) { i.setState({ showConfirmTransferSite: false }); } handleImageExpandClick(i: PostListing, event: any) { event.preventDefault(); i.setState({ imageExpanded: !i.state.imageExpanded }); setupTippy(); } handleViewSource(i: PostListing) { i.setState({ viewSource: !i.state.viewSource }); } handleShowAdvanced(i: PostListing) { i.setState({ showAdvanced: !i.state.showAdvanced }); setupTippy(); } handleShowMoreMobile(i: PostListing) { i.setState({ showMoreMobile: !i.state.showMoreMobile, showAdvanced: !i.state.showAdvanced, }); setupTippy(); } handleShowBody(i: PostListing) { i.setState({ showBody: !i.state.showBody }); setupTippy(); } handleUpvote(i: PostListing) { i.setState({ upvoteLoading: true }); i.props.onPostVote({ post_id: i.postView.post.id, score: newVote(VoteType.Upvote, i.props.post_view.my_vote), auth: myAuthRequired(), }); } handleDownvote(i: PostListing) { i.setState({ downvoteLoading: true }); i.props.onPostVote({ post_id: i.postView.post.id, score: newVote(VoteType.Downvote, i.props.post_view.my_vote), auth: myAuthRequired(), }); } get pointsTippy(): string { const points = i18n.t("number_of_points", { count: Number(this.postView.counts.score), formattedCount: Number(this.postView.counts.score), }); const upvotes = i18n.t("number_of_upvotes", { count: Number(this.postView.counts.upvotes), formattedCount: Number(this.postView.counts.upvotes), }); const downvotes = i18n.t("number_of_downvotes", { count: Number(this.postView.counts.downvotes), formattedCount: Number(this.postView.counts.downvotes), }); return `${points} • ${upvotes} • ${downvotes}`; } get canModOnSelf_(): boolean { return canMod( this.postView.creator.id, this.props.moderators, this.props.admins, undefined, true ); } get canMod_(): boolean { return canMod( this.postView.creator.id, this.props.moderators, this.props.admins ); } get canAdmin_(): boolean { return canAdmin(this.postView.creator.id, this.props.admins); } get creatorIsMod_(): boolean { return isMod(this.postView.creator.id, this.props.moderators); } get creatorIsAdmin_(): boolean { return isAdmin(this.postView.creator.id, this.props.admins); } }