Moderate content in embeds (#3525)

* move info to its own file

* Revert "move info to its own file"

This reverts commit 1d45a2f4034f50cbe9cb25070f954042cdf9127a.

* better way

* all cases

* pass labelInfo to ImageEmbed

* blur avatars

* add back as string

* one more as string

* external embed

* add back as string again
zio/stable
Hailey 2024-04-13 03:18:18 -07:00 committed by GitHub
parent f5bb348bf5
commit 826f6b043c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 91 additions and 17 deletions

View File

@ -9,23 +9,33 @@ import {
AppBskyLabelerDefs, AppBskyLabelerDefs,
} from '@atproto/api' } from '@atproto/api'
import {ComponentChildren, h} from 'preact' import {ComponentChildren, h} from 'preact'
import {useMemo} from 'preact/hooks'
import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg' import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg'
import {CONTENT_LABELS, labelsToInfo} from '../labels'
import {getRkey} from '../utils' import {getRkey} from '../utils'
import {Link} from './link' import {Link} from './link'
export function Embed({content}: {content: AppBskyFeedDefs.PostView['embed']}) { export function Embed({
content,
labels,
}: {
content: AppBskyFeedDefs.PostView['embed']
labels: AppBskyFeedDefs.PostView['labels']
}) {
const labelInfo = useMemo(() => labelsToInfo(labels), [labels])
if (!content) return null if (!content) return null
try { try {
// Case 1: Image // Case 1: Image
if (AppBskyEmbedImages.isView(content)) { if (AppBskyEmbedImages.isView(content)) {
return <ImageEmbed content={content} /> return <ImageEmbed content={content} labelInfo={labelInfo} />
} }
// Case 2: External link // Case 2: External link
if (AppBskyEmbedExternal.isView(content)) { if (AppBskyEmbedExternal.isView(content)) {
return <ExternalEmbed content={content} /> return <ExternalEmbed content={content} labelInfo={labelInfo} />
} }
// Case 3: Record (quote or linked post) // Case 3: Record (quote or linked post)
@ -50,15 +60,22 @@ export function Embed({content}: {content: AppBskyFeedDefs.PostView['embed']}) {
if (AppBskyFeedPost.isRecord(record.value)) { if (AppBskyFeedPost.isRecord(record.value)) {
text = record.value.text text = record.value.text
} }
const isAuthorLabeled = record.author.labels?.some(label =>
CONTENT_LABELS.includes(label.val),
)
return ( return (
<Link <Link
href={`/profile/${record.author.did}/post/${getRkey(record)}`} href={`/profile/${record.author.did}/post/${getRkey(record)}`}
className="transition-colors hover:bg-neutral-100 border rounded-lg p-2 gap-1.5 w-full flex flex-col"> className="transition-colors hover:bg-neutral-100 border rounded-lg p-2 gap-1.5 w-full flex flex-col">
<div className="flex gap-1.5 items-center"> <div className="flex gap-1.5 items-center">
<div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 shrink-0">
<img <img
src={record.author.avatar} src={record.author.avatar}
className="w-4 h-4 rounded-full bg-neutral-300 shrink-0" style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined}
/> />
</div>
<p className="line-clamp-1 text-sm"> <p className="line-clamp-1 text-sm">
<span className="font-bold">{record.author.displayName}</span> <span className="font-bold">{record.author.displayName}</span>
<span className="text-textLight ml-1"> <span className="text-textLight ml-1">
@ -74,7 +91,11 @@ export function Embed({content}: {content: AppBskyFeedDefs.PostView['embed']}) {
return false return false
}) })
.map(embed => ( .map(embed => (
<Embed key={embed.$type} content={embed} /> <Embed
key={embed.$type}
content={embed}
labels={record.labels}
/>
))} ))}
</Link> </Link>
) )
@ -137,15 +158,19 @@ export function Embed({content}: {content: AppBskyFeedDefs.PostView['embed']}) {
} }
// Case 4: Record with media // Case 4: Record with media
if (AppBskyEmbedRecordWithMedia.isView(content)) { if (
AppBskyEmbedRecordWithMedia.isView(content) &&
AppBskyEmbedRecord.isViewRecord(content.record.record)
) {
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Embed content={content.media} /> <Embed content={content.media} labels={labels} />
<Embed <Embed
content={{ content={{
$type: 'app.bsky.embed.record#view', $type: 'app.bsky.embed.record#view',
record: content.record.record, record: content.record.record,
}} }}
labels={content.record.record.labels}
/> />
</div> </div>
) )
@ -168,7 +193,17 @@ function Info({children}: {children: ComponentChildren}) {
) )
} }
function ImageEmbed({content}: {content: AppBskyEmbedImages.View}) { function ImageEmbed({
content,
labelInfo,
}: {
content: AppBskyEmbedImages.View
labelInfo?: string
}) {
if (labelInfo) {
return <Info>{labelInfo}</Info>
}
switch (content.images.length) { switch (content.images.length) {
case 1: case 1:
return ( return (
@ -229,7 +264,13 @@ function ImageEmbed({content}: {content: AppBskyEmbedImages.View}) {
} }
} }
function ExternalEmbed({content}: {content: AppBskyEmbedExternal.View}) { function ExternalEmbed({
content,
labelInfo,
}: {
content: AppBskyEmbedExternal.View
labelInfo?: string
}) {
function toNiceDomain(url: string): string { function toNiceDomain(url: string): string {
try { try {
const urlp = new URL(url) const urlp = new URL(url)
@ -238,6 +279,11 @@ function ExternalEmbed({content}: {content: AppBskyEmbedExternal.View}) {
return url return url
} }
} }
if (labelInfo) {
return <Info>{labelInfo}</Info>
}
return ( return (
<Link <Link
href={content.external.uri} href={content.external.uri}

View File

@ -5,6 +5,7 @@ import replyIcon from '../../assets/bubble_filled_stroke2_corner2_rounded.svg'
import likeIcon from '../../assets/heart2_filled_stroke2_corner0_rounded.svg' import likeIcon from '../../assets/heart2_filled_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg' import logo from '../../assets/logo.svg'
import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg' import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg'
import {CONTENT_LABELS} from '../labels'
import {getRkey, niceDate} from '../utils' import {getRkey, niceDate} from '../utils'
import {Container} from './container' import {Container} from './container'
import {Embed} from './embed' import {Embed} from './embed'
@ -17,6 +18,10 @@ interface Props {
export function Post({thread}: Props) { export function Post({thread}: Props) {
const post = thread.post const post = thread.post
const isAuthorLabeled = post.author.labels?.some(label =>
CONTENT_LABELS.includes(label.val),
)
let record: AppBskyFeedPost.Record | null = null let record: AppBskyFeedPost.Record | null = null
if (AppBskyFeedPost.isRecord(post.record)) { if (AppBskyFeedPost.isRecord(post.record)) {
record = post.record record = post.record
@ -28,10 +33,12 @@ export function Post({thread}: Props) {
<div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}> <div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}>
<div className="flex gap-2.5 items-center"> <div className="flex gap-2.5 items-center">
<Link href={`/profile/${post.author.did}`} className="rounded-full"> <Link href={`/profile/${post.author.did}`} className="rounded-full">
<div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 shrink-0">
<img <img
src={post.author.avatar} src={post.author.avatar}
className="w-10 h-10 rounded-full bg-neutral-300 shrink-0" style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined}
/> />
</div>
</Link> </Link>
<div className="flex-1"> <div className="flex-1">
<Link <Link
@ -52,7 +59,7 @@ export function Post({thread}: Props) {
</Link> </Link>
</div> </div>
<PostContent record={record} /> <PostContent record={record} />
<Embed content={post.embed} /> <Embed content={post.embed} labels={post.labels} />
<time <time
datetime={new Date(post.indexedAt).toISOString()} datetime={new Date(post.indexedAt).toISOString()}
className="text-textLight mt-1 text-sm"> className="text-textLight mt-1 text-sm">

View File

@ -0,0 +1,21 @@
import {AppBskyFeedDefs} from '@atproto/api'
export const CONTENT_LABELS = ['porn', 'sexual', 'nudity', 'graphic-media']
export function labelsToInfo(
labels?: AppBskyFeedDefs.PostView['labels'],
): string | undefined {
const label = labels?.find(label => CONTENT_LABELS.includes(label.val))
switch (label?.val) {
case 'porn':
case 'sexual':
return 'Adult Content'
case 'nudity':
return 'Non-sexual Nudity'
case 'graphic-media':
return 'Graphic Media'
default:
return undefined
}
}