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 againzio/stable
parent
f5bb348bf5
commit
826f6b043c
|
@ -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}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue