import * as ATPAPI from "@atproto/api"; import { AppBskyActorDefs, AppBskyFeedDefs, AppBskyFeedPost, AtUri, type Facet, } from "@atproto/api"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import DOMPurify from "dompurify"; import { useAtom } from "jotai"; import { DropdownMenu } from "radix-ui"; import { HoverCard } from "radix-ui"; import * as React from "react"; import { useEffect, useState } from "react"; import { FORCE_HIDE_LABELS, FORCE_HIDE_LABELS_WHITELISTED_SOURCE, UNAUTHED_PREVENT_OPENING_WARNS } from "~/../policy"; import defaultpfp from "~/../public/defaultpfp.png"; import { getGetHydratedLabelDefs } from "~/hooks/useAutoLabels"; import { useLabelInfo } from "~/hooks/useLabelInfo"; import type { HydratedLabelValueDefinition } from "~/providers/AutoLabelProvider"; //import { useModeration } from "~/hooks/useModeration"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { renderSnack } from "~/routes/__root"; //import { ModerationInner } from "~/routes/moderation"; import { FollowButton, getLocaleLabel, type LabelWithHydratedLocaleName, Mutual } from "~/routes/profile.$did"; import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; //import type { ContentLabel } from "~/types/moderation"; import { appviewUrlAtom, composerAtom, constellationURLAtom, enableAppViewAtom, enableBridgyTextAtom, enableWafrnTextAtom, imgCDNAtom, } from "~/utils/atoms"; import { useGetOneToOneState } from "~/utils/followState"; import { useFastLike } from "~/utils/likeMutationQueue"; import { useHydratedEmbed } from "~/utils/useHydrated"; import { useQueryConstellation, useQueryIdentity, useQueryPost, useQueryProfile, useQuerySingularAVPostQuery, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, } from "~/utils/useQuery"; import { PostEmbeds } from "./PostEmbeds"; import { btnstyle, fullDateTimeFormat, HitSlopButton, randomString, renderTextWithFacets, shortTimeAgo, } from "./UtilityFunctions"; export interface UniversalPostRendererATURILoaderProps { atUri: string; onConstellation?: (data: any) => void; detailed?: boolean; bottomReplyLine?: boolean; topReplyLine?: boolean; bottomBorder?: boolean; feedviewpost?: boolean; repostedby?: string; style?: React.CSSProperties; ref?: React.RefObject; dataIndexPropPass?: number; nopics?: boolean; concise?: boolean; lightboxCallback?: (d: LightboxProps) => void; maxReplies?: number; isQuote?: boolean; filterNoReplies?: boolean; filterMustHaveMedia?: boolean; filterMustBeReply?: boolean; } export function UniversalPostRendererATURILoader({ atUri, onConstellation, detailed = false, bottomReplyLine, topReplyLine, bottomBorder = true, feedviewpost = false, repostedby, style, ref, dataIndexPropPass, nopics, concise, lightboxCallback, maxReplies, isQuote, filterNoReplies, filterMustHaveMedia, filterMustBeReply, }: UniversalPostRendererATURILoaderProps) { const [usesAV] = useAtom(enableAppViewAtom); if (usesAV) { return ( ) } return ( ) } /* todo: - either - put constellation based reply threading or - use a getPostThreadV2 once for quick reply threadings (the post thread page always fetches replies via constellation for complteness) - do the profile pages too */ export function UniversalPostRendererATURILoader_AppView({ atUri, onConstellation, detailed = false, bottomReplyLine, topReplyLine, bottomBorder = true, feedviewpost = false, repostedby, style, ref, dataIndexPropPass, nopics, concise, lightboxCallback, maxReplies, isQuote, filterNoReplies, filterMustHaveMedia, filterMustBeReply, }: UniversalPostRendererATURILoaderProps) { const [avurl] = useAtom(appviewUrlAtom); const navigate = useNavigate(); const parsedaturi = new AtUri(atUri); const { data, isLoading, isEnabled, isError, error } = useQuerySingularAVPostQuery({ aturi: atUri, avurl: avurl }); const thereply = (data?.record as AppBskyFeedPost.Record)?.reply?.parent ?.uri; const feedviewpostreplydid = thereply && !filterNoReplies ? new AtUri(thereply).host : undefined; const replyhookvalue = useQueryIdentity( feedviewpost ? feedviewpostreplydid : undefined, ); const feedviewpostreplyhandle = replyhookvalue?.data?.handle; const aturirepostbydid = repostedby ? new AtUri(repostedby).host : undefined; const repostedbyhookvalue = useQueryIdentity( repostedby ? aturirepostbydid : undefined, ); const feedviewpostrepostedbyhandle = repostedbyhookvalue?.data?.handle; if (!isLoading && data === undefined) { return ( ) } return ( parsedaturi && navigate({ to: "/profile/$did/post/$rkey", params: { did: parsedaturi.host, rkey: parsedaturi.rkey }, }) } onProfileClick={(e) => { e.stopPropagation(); if (parsedaturi) { navigate({ to: "/profile/$did", params: { did: parsedaturi.host }, }); } }} post={data || { uri: atUri, cid: atUri, author: { did: parsedaturi.host, handle: parsedaturi.host, }, record: {}, indexedAt: "", }} // todo: this is bad. just make it so that UPR allows missing data uprrrsauthor={{ ...(data?.author || { did: parsedaturi.host, handle: parsedaturi.host, }), "$type": "app.bsky.actor.defs#profileViewDetailed", }} salt={atUri} bottomReplyLine={bottomReplyLine} topReplyLine={topReplyLine} bottomBorder={bottomBorder} feedviewpost={feedviewpost} feedviewpostreplyhandle={feedviewpostreplyhandle} repostedby={feedviewpostrepostedbyhandle} style={style} ref={ref} dataIndexPropPass={dataIndexPropPass} nopics={nopics} concise={concise} lightboxCallback={lightboxCallback} maxReplies={maxReplies} isQuote={isQuote} constellationLinks={{}} /> ) } export function UniversalPostRendererATURILoader_Microcosm({ atUri, onConstellation, detailed = false, bottomReplyLine, topReplyLine, bottomBorder = true, feedviewpost = false, repostedby, style, ref, dataIndexPropPass, nopics, concise, lightboxCallback, maxReplies, isQuote, filterNoReplies, filterMustHaveMedia, filterMustBeReply, }: UniversalPostRendererATURILoaderProps) { const TEMPLINEAR = true; const parsed = new AtUri(atUri); const did = parsed?.host; const rkey = parsed?.rkey; const { data: postQuery, isLoading: isPostLoading, isError: isPostError, } = useQueryPost(atUri); const { data: resolved } = useQueryIdentity(did || ""); const { data: links } = useQueryConstellation({ method: "/links/all", target: atUri, }); const { data: opProfile } = useQueryProfile( resolved ? `at://${resolved?.did}/app.bsky.actor.profile/self` : undefined, ); const [likes, setLikes] = React.useState(null); const [reposts, setReposts] = React.useState(null); const [replies, setReplies] = React.useState(null); React.useEffect(() => { setLikes( links ? links?.links?.["app.bsky.feed.like"]?.[".subject.uri"]?.records || 0 : null, ); setReposts( links // add the two quote forms as well ? links?.links?.["app.bsky.feed.repost"]?.[".subject.uri"]?.records // .embed.record.uri + links?.links?.["app.bsky.feed.post"]?.[".embed.record.uri"]?.records // .embed.record.record.uri + links?.links?.["app.bsky.feed.post"]?.[".embed.record.record.uri"]?.records || 0 : null, ); setReplies( links ? links?.links?.["app.bsky.feed.post"]?.[".reply.parent.uri"] ?.records || 0 : null, ); }, [links]); const [constellationurl] = useAtom(constellationURLAtom); const infinitequeryresults = useInfiniteQuery({ ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( { constellation: constellationurl, method: "/links", target: atUri, collection: "app.bsky.feed.post", path: ".reply.parent.uri", }, ), enabled: !!atUri && !!maxReplies && !isQuote, }); const { data: repliesData } = infinitequeryresults; useEffect(() => { if (!maxReplies || isQuote || TEMPLINEAR) return; if ( infinitequeryresults.hasNextPage && !infinitequeryresults.isFetchingNextPage ) { console.log("Fetching the next page..."); infinitequeryresults.fetchNextPage(); } }, [TEMPLINEAR, infinitequeryresults, isQuote, maxReplies]); const replyAturis = repliesData ? repliesData.pages.flatMap((page) => page ? page.linking_records.map((record) => { const aturi = `at://${record.did}/${record.collection}/${record.rkey}`; return aturi; }) : [], ) : []; const { oldestOpsReply, oldestOpsReplyElseNewestNonOpsReply } = (() => { if (isQuote || !replyAturis || replyAturis.length === 0 || !maxReplies) return { oldestOpsReply: undefined, oldestOpsReplyElseNewestNonOpsReply: undefined, }; const opdid = new AtUri(atUri).host; const opReplies = replyAturis.filter( (aturi) => new AtUri(aturi).host === opdid, ); if (opReplies.length > 0) { const opreply = opReplies[opReplies.length - 1]; return { oldestOpsReply: opreply, oldestOpsReplyElseNewestNonOpsReply: opreply, }; } else { return { oldestOpsReply: undefined, oldestOpsReplyElseNewestNonOpsReply: replyAturis[0], }; } })(); // placeholder for when a post is missing if (!isPostLoading && !postQuery?.value || isPostError) { if (feedviewpost) { return null // if feed view post then missing post isnt important and just remove it from view } return ( <> {/* todo add reply lines here. */} {/* todo dont let the UPR render the shitty placeholder uri we received */} {/*
*/}
Missing {isQuote ? "Quoted" : ""} Post
); } return ( <> <> {maxReplies !== undefined && maxReplies === 0 && replies && replies > 0 ? ( <> ) : ( <> )} {!isQuote && oldestOpsReplyElseNewestNonOpsReply && ( <> 0} topReplyLine={ (!!(maxReplies && maxReplies - 1 === 0) && !!(replies && replies > 0)) || !!((maxReplies ?? 0) > 1) } bottomBorder={bottomBorder} feedviewpost={feedviewpost} repostedby={repostedby} style={style} ref={ref} dataIndexPropPass={dataIndexPropPass} nopics={nopics} concise={concise} lightboxCallback={lightboxCallback} maxReplies={ maxReplies && maxReplies > 0 ? maxReplies - 1 : undefined } /> )} ); } function MoreReplies({ atUri }: { atUri: string }) { const navigate = useNavigate(); const aturio = new AtUri(atUri); return (
navigate({ to: "/profile/$did/post/$rkey", params: { did: aturio.host, rkey: aturio.rkey }, }) } className="border-b border-gray-200 dark:border-gray-800 flex flex-row px-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors" >
More Replies
); } function getAvatarUrl(opProfile: any, did: string, cdn: string) { const link = opProfile?.value?.avatar?.ref?.["$link"]; if (!link) return null; return `https://${cdn}/img/avatar/plain/${did}/${link}@jpeg`; } export function UniversalPostRendererRawRecordShim({ postRecord, profileRecord, aturi, resolved, likesCount, repostsCount, repliesCount, links, detailed = false, bottomReplyLine = false, topReplyLine = false, bottomBorder = true, feedviewpost = false, repostedby, style, ref, dataIndexPropPass, nopics, concise, lightboxCallback, maxReplies, isQuote, filterNoReplies, filterMustHaveMedia, filterMustBeReply, }: { postRecord: any; profileRecord: any; aturi: string; resolved: any; likesCount?: number | null; repostsCount?: number | null; repliesCount?: number | null; links?: any; detailed?: boolean; bottomReplyLine?: boolean; topReplyLine?: boolean; bottomBorder?: boolean; feedviewpost?: boolean; repostedby?: string; style?: React.CSSProperties; ref?: React.RefObject; dataIndexPropPass?: number; nopics?: boolean; concise?: boolean; lightboxCallback?: (d: LightboxProps) => void; maxReplies?: number; isQuote?: boolean; filterNoReplies?: boolean; filterMustHaveMedia?: boolean; filterMustBeReply?: boolean; }) { const navigate = useNavigate(); const hasEmbed = (postRecord?.value as ATPAPI.AppBskyFeedPost.Record)?.embed; const hasImages = hasEmbed?.$type === "app.bsky.embed.images"; const hasVideo = hasEmbed?.$type === "app.bsky.embed.video"; const isquotewithmedia = hasEmbed?.$type === "app.bsky.embed.recordWithMedia"; const isQuotewithImages = isquotewithmedia && (hasEmbed as ATPAPI.AppBskyEmbedRecordWithMedia.Main)?.media?.$type === "app.bsky.embed.images"; const isQuotewithVideo = isquotewithmedia && (hasEmbed as ATPAPI.AppBskyEmbedRecordWithMedia.Main)?.media?.$type === "app.bsky.embed.video"; const hasMedia = hasEmbed && (hasImages || hasVideo || isQuotewithImages || isQuotewithVideo); const { data: hydratedEmbed, isLoading: isEmbedLoading, error: embedError, } = useHydratedEmbed(postRecord?.value?.embed, resolved?.did); const [imgcdn] = useAtom(imgCDNAtom); const parsedaturi = new AtUri(aturi); const fakeprofileviewbasic = React.useMemo( () => ({ did: resolved?.did || "", handle: resolved?.handle || "", displayName: profileRecord?.value?.displayName || "", avatar: getAvatarUrl(profileRecord, resolved?.did, imgcdn) || "", viewer: undefined, labels: profileRecord?.labels || undefined, verification: undefined, pronouns: profileRecord?.value?.pronouns || undefined, }), [imgcdn, profileRecord, resolved?.did, resolved?.handle], ); const fakeprofileviewdetailed = React.useMemo( () => ({ ...fakeprofileviewbasic, $type: "app.bsky.actor.defs#profileViewDetailed", description: profileRecord?.value?.description || undefined, }), [fakeprofileviewbasic, profileRecord?.value?.description], ); const fakepost = React.useMemo( () => ({ $type: "app.bsky.feed.defs#postView", uri: aturi, cid: postRecord?.cid || "", author: fakeprofileviewbasic, record: postRecord?.value || {}, embed: hydratedEmbed ?? undefined, replyCount: repliesCount ?? 0, repostCount: repostsCount ?? 0, likeCount: likesCount ?? 0, quoteCount: 0, indexedAt: postRecord?.value?.createdAt || "", viewer: undefined, labels: postRecord?.labels || undefined, threadgate: undefined, }), [ aturi, postRecord?.cid, postRecord?.value, postRecord?.labels, fakeprofileviewbasic, hydratedEmbed, repliesCount, repostsCount, likesCount, ], ); const thereply = (fakepost?.record as AppBskyFeedPost.Record)?.reply?.parent ?.uri; const feedviewpostreplydid = thereply && !filterNoReplies ? new AtUri(thereply).host : undefined; const replyhookvalue = useQueryIdentity( feedviewpost ? feedviewpostreplydid : undefined, ); const feedviewpostreplyhandle = replyhookvalue?.data?.handle; const aturirepostbydid = repostedby ? new AtUri(repostedby).host : undefined; const repostedbyhookvalue = useQueryIdentity( repostedby ? aturirepostbydid : undefined, ); const feedviewpostrepostedbyhandle = repostedbyhookvalue?.data?.handle; if (filterNoReplies && thereply) return null; if (filterMustHaveMedia && !hasMedia) return null; if (filterMustBeReply && !thereply) return null; return ( <> parsedaturi && navigate({ to: "/profile/$did/post/$rkey", params: { did: parsedaturi.host, rkey: parsedaturi.rkey }, }) } onProfileClick={(e) => { e.stopPropagation(); if (parsedaturi) { navigate({ to: "/profile/$did", params: { did: parsedaturi.host }, }); } }} post={fakepost} uprrrsauthor={fakeprofileviewdetailed} salt={aturi} bottomReplyLine={bottomReplyLine} topReplyLine={topReplyLine} bottomBorder={bottomBorder} feedviewpost={feedviewpost} feedviewpostreplyhandle={feedviewpostreplyhandle} repostedby={feedviewpostrepostedbyhandle} style={style} ref={ref} dataIndexPropPass={dataIndexPropPass} nopics={nopics} concise={concise} lightboxCallback={lightboxCallback} maxReplies={maxReplies} isQuote={isQuote} constellationLinks={links} /> ); } export function UniversalPostRenderer({ post, uprrrsauthor, onPostClick, onProfileClick, expanded, isQuote, extraOptionalItemInfo, bottomReplyLine, topReplyLine, salt, bottomBorder = true, feedviewpost, feedviewpostreplyhandle, depth = 0, repostedby, style, ref, dataIndexPropPass, nopics, concise, lightboxCallback, maxReplies, constellationLinks, referral, }: { post: AppBskyFeedDefs.PostView; uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed; onPostClick?: (e: React.MouseEvent) => void; onProfileClick?: (e: React.MouseEvent) => void; expanded?: boolean; isQuote?: boolean; extraOptionalItemInfo?: AppBskyFeedDefs.FeedViewPost; bottomReplyLine?: boolean; topReplyLine?: boolean; salt: string; bottomBorder?: boolean; feedviewpost?: boolean; feedviewpostreplyhandle?: string; depth?: number; repostedby?: string; style?: React.CSSProperties; ref?: React.RefObject; dataIndexPropPass?: number; nopics?: boolean; concise?: boolean; lightboxCallback?: (d: LightboxProps) => void; maxReplies?: number; constellationLinks?: any; referral?: string[]; }) { // todo move moderation to one of the UniversalPostRenderer wrapper components, and not the pure renderer component. please. thanks // todo please move all moderation including labeling and blocks into a wrapper component please i beg you const subjects = [ post.author.did, `at://${post.author.did}/app.bsky.actor.profile/self`, post.uri, ] // let { // results: labelResults, // hydratedLabelDefs, // } = useAutoLabels({ // subjects, // type: "post", // or whatever you’re keying on for now // }) const labelResults = new Map() const hydratedLabelDefs = new Map(); const ghld = getGetHydratedLabelDefs(hydratedLabelDefs) const accountResult = labelResults.get(post.author.did) const profileResult = labelResults.get( `at://${post.author.did}/app.bsky.actor.profile/self`, ) const postResult = labelResults.get(post.uri) const accountLabelVerdict = accountResult?.labelVerdict ?? "unknown" const authorLabels = accountResult?.labels ?? [] const profileLabelVerdict = profileResult?.labelVerdict ?? "unknown" const profileLabels = profileResult?.labels ?? [] const postLabelVerdict = postResult?.labelVerdict ?? "unknown" const contentLabels = postResult?.labels ?? [] const combinedLabels: ATPAPI.ComAtprotoLabelDefs.Label[] = [] //[...authorLabels, ...profileLabels, ...contentLabels] const authorModUnknown = false //accountLabelVerdict === "unknown"; const profileModUnknown = false //profileLabelVerdict === "unknown"; const contentModUnknown = false //postLabelVerdict === "unknown"; const authorModLoading = false //accountLabelVerdict === "loading"; const profileModLoading = false //profileLabelVerdict === "loading"; const contentModLoading = false //postLabelVerdict === "loading"; const authorModError = false //accountLabelVerdict === "error"; const profileModError = false //profileLabelVerdict === "error"; const contentModError = false //postLabelVerdict === "error"; const verdictDebugString = `accountLabelVerdict: ${accountLabelVerdict}, profileLabelVerdict: ${profileLabelVerdict}, postLabelVerdict: ${postLabelVerdict}` //const verdictDebugStringCauses = const strictModerationUnknown = authorModUnknown || profileModUnknown || contentModUnknown const strictModerationLoading = authorModLoading || profileModLoading || contentModLoading const strictModerationError = authorModError || profileModError || contentModError const strictModerationDontShow = strictModerationUnknown || strictModerationLoading || strictModerationError const hideAuthorLabels = authorLabels.filter( (label) => ghld(label.src, label.val)?.pref === "hide", ); const warnAuthorLabels = authorLabels.filter( (label) => ghld(label.src, label.val)?.pref === "warn", ); // const errorAuthorLabels = authorLabels.filter( // //(label) => ghld(label.src,label.val)?.severity === "hide", // ); const hideProfileLabels = profileLabels.filter( (label) => ghld(label.src, label.val)?.pref === "hide", ); const warnProfileLabels = profileLabels.filter( (label) => ghld(label.src, label.val)?.pref === "warn", ); const hideContentLabels = contentLabels.filter( (label) => ghld(label.src, label.val)?.pref === "hide", ); const warnContentLabels = contentLabels.filter( (label) => ghld(label.src, label.val)?.pref === "warn", ); // add user pronouns const pronoun = post.author.pronouns || undefined const informCombinedLabels: LabelWithHydratedLocaleName[] = combinedLabels.flatMap( (label) => { if (ghld(label.src, label.val)?.severity === "inform" && ghld(label.src, label.val)?.pref === "warn") { return [{ ...label, name: getLocaleLabel(ghld(label.src, label.val))?.name || label.val }] } return [] }, ); const parsed = new AtUri(post.uri); const navigate = useNavigate(); const [hasRetweeted, setHasRetweeted] = useState( post.viewer?.repost ? true : false, ); const [, setComposerPost] = useAtom(composerAtom); const { agent, status } = useAuth(); const [retweetUri, setRetweetUri] = useState( post.viewer?.repost, ); const { liked, toggle, backfill } = useFastLike(post.uri, post.cid); const agentDid = agent?.did; const authorDid = post.author.did; const userBlocksAuthor = useGetOneToOneState( agentDid && authorDid ? { target: authorDid, user: agentDid, collection: "app.bsky.graph.block", path: ".subject", } : undefined, ); const authorBlocksUser = useGetOneToOneState( agentDid && authorDid ? { target: agentDid, user: authorDid, collection: "app.bsky.graph.block", path: ".subject", } : undefined, ); const repostOrUnrepostPost = async () => { if (!agent) { console.error("Agent is null or undefined"); return; } if (hasRetweeted) { if (retweetUri) { await agent.deleteRepost(retweetUri); setHasRetweeted(false); } } else { const { uri } = await agent.repost(post.uri, post.cid); setRetweetUri(uri); setHasRetweeted(true); } }; const isRepost = repostedby ? repostedby : extraOptionalItemInfo ? AppBskyFeedDefs.isReasonRepost(extraOptionalItemInfo.reason) ? extraOptionalItemInfo.reason?.by.displayName : undefined : undefined; const isReply = extraOptionalItemInfo ? extraOptionalItemInfo.reply : undefined; const emergencySalt = randomString(); const [showBridgyText] = useAtom(enableBridgyTextAtom); const [showWafrnText] = useAtom(enableWafrnTextAtom); const unfedibridgy = (post.record as { bridgyOriginalText?: string }) .bridgyOriginalText; const unfediwafrnPartial = (post.record as { fullText?: string }).fullText; const unfediwafrnTags = (post.record as { fullTags?: string }).fullTags; const unfediwafrnUnHost = (post.record as { fediverseId?: string }) .fediverseId; const undfediwafrnHost = unfediwafrnUnHost ? new URL(unfediwafrnUnHost).hostname : undefined; const tags = unfediwafrnTags ? unfediwafrnTags .split("\n") .map((t) => t.trim()) .filter(Boolean) : undefined; const links = tags ? tags .map((tag) => { const encoded = encodeURIComponent(tag); return `#${tag.replaceAll(" ", "-")}`; }) .join("
") : ""; const unfediwafrn = unfediwafrnPartial ? unfediwafrnPartial + (links ? `
${links}` : "") : undefined; const fedi = (showBridgyText ? unfedibridgy : undefined) ?? (showWafrnText ? unfediwafrn : undefined); const isMainItem = false; const setMainItem = (any: any) => { }; const hideWarnsWhenUnauthed = UNAUTHED_PREVENT_OPENING_WARNS && status === "signedOut"; const showContentWarning = warnContentLabels.length > 0; const [isOpen, setIsOpen] = useState(!showContentWarning); const [hasUserTouchedToggleYet, setHasUserTouchedToggleYet] = useState(false); // Force Hiddens from host policy const isForceHiddenAuthor = authorLabels.some((label) => { return ( FORCE_HIDE_LABELS.has(label.val) && FORCE_HIDE_LABELS_WHITELISTED_SOURCE.has(label.src) ); }); const isForceHiddenProfile = profileLabels.some((label) => { return ( FORCE_HIDE_LABELS.has(label.val) && FORCE_HIDE_LABELS_WHITELISTED_SOURCE.has(label.src) ); }); const isForceHiddenPost = contentLabels.some((label) => { return ( FORCE_HIDE_LABELS.has(label.val) && FORCE_HIDE_LABELS_WHITELISTED_SOURCE.has(label.src) ); }); const isForceHidden = isForceHiddenAuthor || isForceHiddenProfile || isForceHiddenPost useEffect(() => { if (!hasUserTouchedToggleYet && showContentWarning) { // eslint-disable-next-line react-hooks/set-state-in-effect setIsOpen(false); } }, [hasUserTouchedToggleYet, showContentWarning]) console.log("HLLO HLLO HisForceHidden post UPR" + post.uri + post.author.did + isForceHidden, "1what", contentLabels, "2what", authorLabels) // if (hideAuthorLabels.length > 0 || hideContentLabels.length > 0 || isForceHidden || strictModerationDontShow) { // return ( //
// DEBUG LOADING LABELS // {post.uri} // {verdictDebugString} //
// ); // } // if ( isForceHidden ) { // return ( //
// Post Hidden //
// ) // } // todo respect the blur label def // todo scrap the verdict system and rename it into what it is (loading state) const redactWhileLoadingAuthor = authorModLoading || authorModError || authorModUnknown const redactWhileLoadingProfile = profileModLoading || profileModError || profileModUnknown const redactWhileLoadingPost = contentModLoading || contentModError || contentModUnknown const redactWhileLoadingBlock = userBlocksAuthor.isLoading || authorBlocksUser.isLoading const redactWhileLoadingSome = redactWhileLoadingAuthor || redactWhileLoadingProfile || redactWhileLoadingPost || redactWhileLoadingBlock /** * maybe rules: * if author is loading, hide everything * if post is loading, hide text and embeds * if profile is loading, hide pfp */ // the || !post.record?.createdAt is so that users cant imply theyre replying to a non existant post by a user // if the post doesnt exist, dont render the name or pfp const redactWhileLoading_name = redactWhileLoadingAuthor || !post.record?.createdAt || redactWhileLoadingBlock const redactWhileLoading_content = redactWhileLoadingAuthor || redactWhileLoadingPost || !post.record?.createdAt || redactWhileLoadingBlock const redactWhileLoading_pfp = redactWhileLoadingAuthor || redactWhileLoadingProfile || !post.record?.createdAt || redactWhileLoadingBlock const redactFinalBlock = userBlocksAuthor.uris.length > 0 || authorBlocksUser.uris.length > 0 const redactFinalAuthor = hideAuthorLabels.length > 0 || isForceHiddenAuthor || redactFinalBlock const redactFinalProfile = hideProfileLabels.length > 0 || isForceHiddenProfile || redactFinalBlock const redactFinalPost = hideContentLabels.length > 0 || isForceHiddenPost || redactFinalBlock const redactFinalSome = redactFinalAuthor || redactFinalProfile || redactFinalPost || redactFinalBlock // todo consider if adding an explicit "post removed" visible component is better for this //if (redactFinalSome) return null // todo preserve reply lines // todo share the component with the Missing post from above if (redactFinalSome) { if (feedviewpost) { return null // if feed view post then moderated post isnt important and just remove it from view } return (
{ setMainItem({ post: post }); onPostClick(e); } : () => { setMainItem({ post: post }); } : undefined }>
Moderated Post
    {userBlocksAuthor.uris.length > 0 && (
  • User Blocked by You
  • )} {authorBlocksUser.uris.length > 0 && (
  • User Blocking You
  • )} {hideAuthorLabels.length > 0 && (<>{hideAuthorLabels.map((label) => { return
  • Author Label: {getLocaleLabel(ghld(label.src, label.val))?.name || label.val}
  • })})} {hideProfileLabels.length > 0 && (<>{hideProfileLabels.map((label) => { return
  • Profile Label: {getLocaleLabel(ghld(label.src, label.val))?.name || label.val}
  • })})} {hideContentLabels.length > 0 && (<>{hideContentLabels.map((label) => { return
  • Post Label: {getLocaleLabel(ghld(label.src, label.val))?.name || label.val}
  • })})}
) } // ${redactWhileLoadingSome && "blur"} return (
{/* {JSON.stringify(post, null, 2)} */}
{ setMainItem({ post: post }); onPostClick(e); } : () => { setMainItem({ post: post }); } : undefined } style={{ opacity: "1 !important", background: "transparent", paddingLeft: isQuote ? 12 : 16, paddingRight: isQuote ? 12 : 16, paddingTop: isRepost ? 10 : isQuote ? 12 : topReplyLine ? 8 : 16, paddingBottom: 0, fontFamily: "system-ui, sans-serif", position: "relative", borderBottomWidth: bottomBorder ? (isQuote ? 0 : 1) : 0, }} className="border-gray-200 dark:border-gray-800" > {isRepost && (
Reposted by @{isRepost}
)} {!isQuote && (
)}
{redactWhileLoading_pfp ? (
) : ( avatar ) }
{redactWhileLoading_pfp ? (
) : ( avatar ) }
{redactWhileLoading_name ? "Person Display Name" : (post.author.displayName || post.author.handle)}
@ {redactWhileLoading_name ? "person.placeholder" : post.author.handle}
{uprrrsauthor?.description && (
{uprrrsauthor.description}
)}
{bottomReplyLine && (
)}
{redactWhileLoading_name ? "Person Display Name" : post.author.displayName || post.author.handle} {post.author.verification?.verifiedStatus == "valid" && ( )} @{redactWhileLoading_name ? "person.placeholder" : post.author.handle}
· {shortTimeAgo(post.indexedAt)}
{/* */} {authorModLoading ? (
{/*
/ avatar / loading badges...
*/}
) : (
{pronoun && ( )} {informCombinedLabels.map((label, index) => ( ))}
)} {!!feedviewpostreplyhandle && (
Reply to @{feedviewpostreplyhandle}
)} {/* */} {/* todo migrate cw stuff to the new useAutoLabels system */} {showContentWarning && ( { e.stopPropagation(); setHasUserTouchedToggleYet(true); if (!hideWarnsWhenUnauthed) { setIsOpen(!isOpen) } }} /> )} {isOpen && (<>
{fedi ? ( <> ) : ( <> {renderTextWithFacets({ text: (post.record as { text?: string }).text ?? "", facets: (post.record.facets as Facet[]) ?? [], navigate: navigate, })} )}
{post.embed && depth < 1 && !concise ? ( ) : null} {post.embed && depth > 0 && ( <>
(there is an embed here thats too deep to render)
)} )}
<> {expanded && (
{fullDateTimeFormat(post.indexedAt)}
)} {!isQuote && (
{ setComposerPost({ kind: "reply", parent: post.uri }); }} style={{ ...btnstyle, }} className={redactWhileLoading_content && "animate-pulse blur" || undefined} > {post.replyCount}
{hasRetweeted ? ( ) : ( )} {post.repostCount ?? 0}
{hasRetweeted ? "Undo Repost" : "Repost"} { setComposerPost({ kind: "quote", subject: post.uri, }); }} className="px-3 py-2 text-sm flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700" > Quote
{ toggle(); }} style={{ ...btnstyle, ...(liked ? { color: "#EC4899" } : {}), }} className={redactWhileLoading_content && "animate-pulse blur" || undefined} > {liked ? ( ) : ( )} {(post.likeCount || 0) + (liked ? 1 : 0)}
{ e.stopPropagation(); try { await navigator.clipboard.writeText( "https://bsky.app" + "/profile/" + post.author.handle + "/post/" + post.uri.split("/").pop(), ); renderSnack({ title: "Copied to clipboard!", }); } catch (_e) { renderSnack({ title: "Failed to copy link", }); } }} style={{ ...btnstyle, }} > { renderSnack({ title: "Not implemented yet...", }); }} >
)}
); } enum PostEmbedViewContext { ThreadHighlighted = "ThreadHighlighted", Feed = "Feed", FeedEmbedRecordWithMedia = "FeedEmbedRecordWithMedia", } export function ContentWarning({ unauthedgate, labels, isOpen, onPress, }: { unauthedgate?: boolean; labels: ATPAPI.ComAtprotoLabelDefs.Label[]; isOpen: boolean; onPress: React.MouseEventHandler; }) { const { getLabelInfo } = useLabelInfo(); // Pre-calculate text for cleaner JSX const labelText = labels .map((label) => getLabelInfo(label.src, label.val).name) .join(", "); return (
{/* Icon Container */}
{/* Label Text */} {labelText}
{/* Chevron */}
{unauthedgate ? "please login to view" : isOpen ? "hide" : "show"} {!unauthedgate && ()}
); } export function SmallAuthorLabelBadge({ label, large, }: { label: LabelWithHydratedLocaleName; large?: boolean; }) { /* -{" "} {ghld(label.src,label.val)?.severity} (from {label.sourceDid}) */ //const info = getLabelInfo(label.src, label.val); const [imgcdn] = useAtom(imgCDNAtom); const { data: opProfile } = useQueryProfile( `at://${label.src}/app.bsky.actor.profile/self`, ); const resolvedpfp = getAvatarUrl(opProfile, label.src, imgcdn); return ( ); } // todo add click event to explain the label or soemthing export function SmallAuthorLabelBadgeInner({ resolvedpfp, text, large, disablepfp = false, }: { resolvedpfp?: string; text: string; large?: boolean; disablepfp?: boolean; }) { return (
{!disablepfp && (avatar)} {text}
); }