about summary refs log tree commit diff
path: root/bskyembed
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-04-13 03:18:18 -0700
committerGitHub <noreply@github.com>2024-04-13 11:18:18 +0100
commit826f6b043ca73f3cc459fbac62ae6de5f82e362b (patch)
tree296cd847fa76d2d0691ffdeaa59b6731714cef47 /bskyembed
parentf5bb348bf51df6f6d35eb23cdf771c184d77fec4 (diff)
downloadvoidsky-826f6b043ca73f3cc459fbac62ae6de5f82e362b.tar.zst
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
Diffstat (limited to 'bskyembed')
-rw-r--r--bskyembed/src/components/embed.tsx70
-rw-r--r--bskyembed/src/components/post.tsx17
-rw-r--r--bskyembed/src/labels.ts21
3 files changed, 91 insertions, 17 deletions
diff --git a/bskyembed/src/components/embed.tsx b/bskyembed/src/components/embed.tsx
index 2f9f6b3cd..d88019965 100644
--- a/bskyembed/src/components/embed.tsx
+++ b/bskyembed/src/components/embed.tsx
@@ -9,23 +9,33 @@ import {
   AppBskyLabelerDefs,
 } from '@atproto/api'
 import {ComponentChildren, h} from 'preact'
+import {useMemo} from 'preact/hooks'
 
 import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg'
+import {CONTENT_LABELS, labelsToInfo} from '../labels'
 import {getRkey} from '../utils'
 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
 
   try {
     // Case 1: Image
     if (AppBskyEmbedImages.isView(content)) {
-      return <ImageEmbed content={content} />
+      return <ImageEmbed content={content} labelInfo={labelInfo} />
     }
 
     // Case 2: External link
     if (AppBskyEmbedExternal.isView(content)) {
-      return <ExternalEmbed content={content} />
+      return <ExternalEmbed content={content} labelInfo={labelInfo} />
     }
 
     // Case 3: Record (quote or linked post)
@@ -50,15 +60,22 @@ export function Embed({content}: {content: AppBskyFeedDefs.PostView['embed']}) {
         if (AppBskyFeedPost.isRecord(record.value)) {
           text = record.value.text
         }
+
+        const isAuthorLabeled = record.author.labels?.some(label =>
+          CONTENT_LABELS.includes(label.val),
+        )
+
         return (
           <Link
             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">
             <div className="flex gap-1.5 items-center">
-              <img
-                src={record.author.avatar}
-                className="w-4 h-4 rounded-full bg-neutral-300 shrink-0"
-              />
+              <div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 shrink-0">
+                <img
+                  src={record.author.avatar}
+                  style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined}
+                />
+              </div>
               <p className="line-clamp-1 text-sm">
                 <span className="font-bold">{record.author.displayName}</span>
                 <span className="text-textLight ml-1">
@@ -74,7 +91,11 @@ export function Embed({content}: {content: AppBskyFeedDefs.PostView['embed']}) {
                 return false
               })
               .map(embed => (
-                <Embed key={embed.$type} content={embed} />
+                <Embed
+                  key={embed.$type}
+                  content={embed}
+                  labels={record.labels}
+                />
               ))}
           </Link>
         )
@@ -137,15 +158,19 @@ export function Embed({content}: {content: AppBskyFeedDefs.PostView['embed']}) {
     }
 
     // Case 4: Record with media
-    if (AppBskyEmbedRecordWithMedia.isView(content)) {
+    if (
+      AppBskyEmbedRecordWithMedia.isView(content) &&
+      AppBskyEmbedRecord.isViewRecord(content.record.record)
+    ) {
       return (
         <div className="flex flex-col gap-2">
-          <Embed content={content.media} />
+          <Embed content={content.media} labels={labels} />
           <Embed
             content={{
               $type: 'app.bsky.embed.record#view',
               record: content.record.record,
             }}
+            labels={content.record.record.labels}
           />
         </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) {
     case 1:
       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 {
     try {
       const urlp = new URL(url)
@@ -238,6 +279,11 @@ function ExternalEmbed({content}: {content: AppBskyEmbedExternal.View}) {
       return url
     }
   }
+
+  if (labelInfo) {
+    return <Info>{labelInfo}</Info>
+  }
+
   return (
     <Link
       href={content.external.uri}
diff --git a/bskyembed/src/components/post.tsx b/bskyembed/src/components/post.tsx
index dcbf3e336..d0eaf228a 100644
--- a/bskyembed/src/components/post.tsx
+++ b/bskyembed/src/components/post.tsx
@@ -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 logo from '../../assets/logo.svg'
 import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg'
+import {CONTENT_LABELS} from '../labels'
 import {getRkey, niceDate} from '../utils'
 import {Container} from './container'
 import {Embed} from './embed'
@@ -17,6 +18,10 @@ interface Props {
 export function Post({thread}: Props) {
   const post = thread.post
 
+  const isAuthorLabeled = post.author.labels?.some(label =>
+    CONTENT_LABELS.includes(label.val),
+  )
+
   let record: AppBskyFeedPost.Record | null = null
   if (AppBskyFeedPost.isRecord(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 gap-2.5 items-center">
           <Link href={`/profile/${post.author.did}`} className="rounded-full">
-            <img
-              src={post.author.avatar}
-              className="w-10 h-10 rounded-full bg-neutral-300 shrink-0"
-            />
+            <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 shrink-0">
+              <img
+                src={post.author.avatar}
+                style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined}
+              />
+            </div>
           </Link>
           <div className="flex-1">
             <Link
@@ -52,7 +59,7 @@ export function Post({thread}: Props) {
           </Link>
         </div>
         <PostContent record={record} />
-        <Embed content={post.embed} />
+        <Embed content={post.embed} labels={post.labels} />
         <time
           datetime={new Date(post.indexedAt).toISOString()}
           className="text-textLight mt-1 text-sm">
diff --git a/bskyembed/src/labels.ts b/bskyembed/src/labels.ts
new file mode 100644
index 000000000..ff3d91bc7
--- /dev/null
+++ b/bskyembed/src/labels.ts
@@ -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
+  }
+}