Refactor to new arch
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: [
|
||||||
|
'@typescript-eslint',
|
||||||
|
'react',
|
||||||
|
'simple-import-sort',
|
||||||
|
'eslint-plugin-react-compiler',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'react/no-unescaped-entities': 0,
|
||||||
|
'react/prop-types': 0,
|
||||||
|
'react-native/no-inline-styles': 0,
|
||||||
|
'simple-import-sort/imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: [
|
||||||
|
// Side effect imports.
|
||||||
|
['^\\u0000'],
|
||||||
|
// Node.js builtins prefixed with `node:`.
|
||||||
|
['^node:'],
|
||||||
|
// Packages.
|
||||||
|
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
|
||||||
|
// React/React Native priortized, followed by expo
|
||||||
|
// Followed by all packages excluding unprefixed relative ones
|
||||||
|
[
|
||||||
|
'^(react\\/(.*)$)|^(react$)|^(react-native(.*)$)',
|
||||||
|
'^(expo(.*)$)|^(expo$)',
|
||||||
|
'^(?!(?:alf|components|lib|locale|logger|platform|screens|state|view)(?:$|\\/))@?\\w',
|
||||||
|
],
|
||||||
|
// Relative imports.
|
||||||
|
// Ideally, anything that starts with a dot or #
|
||||||
|
// due to unprefixed relative imports being used, we whitelist the relative paths we use
|
||||||
|
// (?:$|\\/) matches end of string or /
|
||||||
|
[
|
||||||
|
'^(?:#\\/)?(?:lib|state|logger|platform|locale)(?:$|\\/)',
|
||||||
|
'^(?:#\\/)?view(?:$|\\/)',
|
||||||
|
'^(?:#\\/)?screens(?:$|\\/)',
|
||||||
|
'^(?:#\\/)?alf(?:$|\\/)',
|
||||||
|
'^(?:#\\/)?components(?:$|\\/)',
|
||||||
|
'^#\\/',
|
||||||
|
'^\\.',
|
||||||
|
],
|
||||||
|
// anything else - hopefully we don't have any of these
|
||||||
|
['^'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'simple-import-sort/exports': 'error',
|
||||||
|
'react-compiler/react-compiler': 'warn',
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
name: '@atproto/api',
|
||||||
|
importNames: ['moderatePost'],
|
||||||
|
message:
|
||||||
|
'Please use `moderatePost_wrapped` from `#/lib/moderatePost_wrapped` instead.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
'coverage',
|
||||||
|
'*.lock',
|
||||||
|
'.husky',
|
||||||
|
'*.html',
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import {AppBskyFeedDefs, moderateFeedGenerator} from '@atproto/api'
|
||||||
|
|
||||||
|
import {ModeratorData} from '../data/getModeratorData.js'
|
||||||
|
import {PostData} from '../data/getPostData.js'
|
||||||
|
import {atoms as a, theme as t} from '../theme/index.js'
|
||||||
|
import {formatCount} from '../util/formatCount.js'
|
||||||
|
import {getModerationCauseInfo} from '../util/getModerationCauseInfo.js'
|
||||||
|
import {Box} from './Box.js'
|
||||||
|
import {Image} from './Image.js'
|
||||||
|
import {ModeratedEmbed} from './ModeratedEmbed.js'
|
||||||
|
import {Text} from './Text.js'
|
||||||
|
|
||||||
|
export function FeedCard({
|
||||||
|
embed,
|
||||||
|
data,
|
||||||
|
moderatorData,
|
||||||
|
}: {
|
||||||
|
embed: AppBskyFeedDefs.GeneratorView
|
||||||
|
data: PostData
|
||||||
|
moderatorData: ModeratorData
|
||||||
|
}) {
|
||||||
|
const feedModeration = moderateFeedGenerator(
|
||||||
|
embed,
|
||||||
|
moderatorData.moderationOptions,
|
||||||
|
)
|
||||||
|
const modui = feedModeration.ui('contentList')
|
||||||
|
const info = getModerationCauseInfo({
|
||||||
|
cause: modui.blurs.at(0),
|
||||||
|
moderatorData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
return <ModeratedEmbed info={info} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const {avatar, displayName, likeCount = 0, creator} = embed
|
||||||
|
const image = data.images.get(avatar || '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
cx={[
|
||||||
|
a.w_full,
|
||||||
|
a.gap_sm,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.p_md,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
{image && (
|
||||||
|
<Image
|
||||||
|
image={image}
|
||||||
|
cx={[
|
||||||
|
a.rounded_sm,
|
||||||
|
{
|
||||||
|
width: '40px',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box cx={[a.pt_2xs]}>
|
||||||
|
<Text cx={[a.text_md, a.font_bold, a.pb_2xs]}>{displayName}</Text>
|
||||||
|
<Text cx={[a.text_sm, a.leading_snug]}>By @{creator.handle}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{likeCount > 1 && (
|
||||||
|
<Text cx={[a.text_sm, a.leading_snug]}>
|
||||||
|
Liked by {formatCount(likeCount)} users
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import {Image as ImageSource} from '../data/getPostData.js'
|
import {Image as ImageSource} from '../data/getPostData.js'
|
||||||
import {style as s} from '../theme/index.js'
|
import {atoms as a, style as s, theme as t} from '../theme/index.js'
|
||||||
|
import {Box} from './Box.js'
|
||||||
|
|
||||||
export type ImageProps = Omit<
|
export type ImageProps = Omit<
|
||||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||||
@@ -21,3 +22,28 @@ export function Image({image, cx, ...rest}: ImageProps) {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SquareImage({image}: {image: ImageSource}) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
cx={[
|
||||||
|
a.relative,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.w_full,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
{paddingTop: '100%'},
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
image={image}
|
||||||
|
cx={[
|
||||||
|
a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
{
|
||||||
|
objectFit: 'cover',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import {Image as ImageSource} from '../data/getPostData.js'
|
||||||
|
import {atoms as a, theme as t} from '../theme/index.js'
|
||||||
|
import {toShortUrl} from '../util/toShortUrl.js'
|
||||||
|
import {Box} from './Box.js'
|
||||||
|
import {Image} from './Image.js'
|
||||||
|
import {Text} from './Text.js'
|
||||||
|
|
||||||
|
export function LinkCard({
|
||||||
|
image,
|
||||||
|
uri,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
image?: ImageSource
|
||||||
|
uri?: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
cx={[
|
||||||
|
a.w_full,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
{image && (
|
||||||
|
<Box
|
||||||
|
cx={[
|
||||||
|
a.relative,
|
||||||
|
a.w_full,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
{paddingTop: (630 / 1200) * 100 + '%'},
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
image={image}
|
||||||
|
cx={[
|
||||||
|
a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
{
|
||||||
|
objectFit: 'cover',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box cx={[a.p_md, t.atoms.bg]}>
|
||||||
|
{uri && (
|
||||||
|
<Text cx={[a.text_xs, a.pb_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
{toShortUrl(uri)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text cx={[a.text_md, a.font_bold, a.pb_xs]}>{title}</Text>
|
||||||
|
<Text cx={[a.text_sm, a.leading_snug]}>{description}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {AppBskyGraphDefs, moderateUserList} from '@atproto/api'
|
||||||
|
|
||||||
|
import {ModeratorData} from '../data/getModeratorData.js'
|
||||||
|
import {PostData} from '../data/getPostData.js'
|
||||||
|
import {atoms as a, theme as t} from '../theme/index.js'
|
||||||
|
import {getModerationCauseInfo} from '../util/getModerationCauseInfo.js'
|
||||||
|
import {Box} from './Box.js'
|
||||||
|
import {Image} from './Image.js'
|
||||||
|
import {ModeratedEmbed} from './ModeratedEmbed.js'
|
||||||
|
import {Text} from './Text.js'
|
||||||
|
|
||||||
|
export function ListCard({
|
||||||
|
embed,
|
||||||
|
data,
|
||||||
|
moderatorData,
|
||||||
|
}: {
|
||||||
|
embed: AppBskyGraphDefs.ListView
|
||||||
|
data: PostData
|
||||||
|
moderatorData: ModeratorData
|
||||||
|
}) {
|
||||||
|
const listModeration = moderateUserList(
|
||||||
|
embed,
|
||||||
|
moderatorData.moderationOptions,
|
||||||
|
)
|
||||||
|
const modui = listModeration.ui('contentList')
|
||||||
|
const info = getModerationCauseInfo({
|
||||||
|
cause: modui.blurs.at(0),
|
||||||
|
moderatorData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
return <ModeratedEmbed info={info} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const {avatar, name, creator} = embed
|
||||||
|
const image = data.images.get(avatar || '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
cx={[
|
||||||
|
a.w_full,
|
||||||
|
a.gap_sm,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.p_md,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
{image && (
|
||||||
|
<Image
|
||||||
|
image={image}
|
||||||
|
cx={[
|
||||||
|
a.rounded_sm,
|
||||||
|
{
|
||||||
|
width: '40px',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box cx={[a.pt_2xs]}>
|
||||||
|
<Text cx={[a.text_md, a.font_bold, a.pb_2xs]}>{name}</Text>
|
||||||
|
<Text cx={[a.text_sm, a.leading_snug]}>By @{creator.handle}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {atoms as a, theme as t} from '../theme/index.js'
|
||||||
|
import {ModerationCauseInfo} from '../util/getModerationCauseInfo.js'
|
||||||
|
import {Box} from './Box.js'
|
||||||
|
import {Text} from './Text.js'
|
||||||
|
|
||||||
|
export function ModeratedEmbed({info}: {info: ModerationCauseInfo}) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
cx={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.p_md,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<info.icon size={20} fill={t.atoms.text_contrast_low.color} />
|
||||||
|
<Box cx={[a.gap_xs]}>
|
||||||
|
<Text cx={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
|
{info.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,42 +1,22 @@
|
|||||||
/* eslint-disable bsky-internal/avoid-unwrapped-text, react-native-a11y/has-valid-accessibility-ignores-invert-colors */
|
|
||||||
import React from 'react'
|
|
||||||
import {
|
import {
|
||||||
AppBskyEmbedExternal,
|
|
||||||
AppBskyEmbedImages,
|
|
||||||
AppBskyEmbedRecord,
|
|
||||||
AppBskyEmbedRecordWithMedia,
|
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AppBskyGraphDefs,
|
|
||||||
AppBskyGraphStarterpack,
|
|
||||||
moderateFeedGenerator,
|
|
||||||
moderateUserList,
|
|
||||||
ModerationDecision,
|
|
||||||
RichText as RichTextApi,
|
RichText as RichTextApi,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {ModeratorData} from '../data/getModeratorData.js'
|
import {ModeratorData} from '../data/getModeratorData.js'
|
||||||
import {Image as ImageSource, PostData} from '../data/getPostData.js'
|
import {PostData} from '../data/getPostData.js'
|
||||||
import {atoms as a, gradient, theme as t} from '../theme/index.js'
|
import {atoms as a, gradient, theme as t} from '../theme/index.js'
|
||||||
import {formatCount} from '../util/formatCount.js'
|
import {formatCount} from '../util/formatCount.js'
|
||||||
import {formatDate} from '../util/formatDate.js'
|
import {formatDate} from '../util/formatDate.js'
|
||||||
import {
|
|
||||||
getModerationCauseInfo,
|
|
||||||
ModerationCauseInfo,
|
|
||||||
} from '../util/getModerationCauseInfo.js'
|
|
||||||
import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js'
|
|
||||||
import {moderatePost} from '../util/moderatePost.js'
|
import {moderatePost} from '../util/moderatePost.js'
|
||||||
import {toShortUrl} from '../util/toShortUrl.js'
|
|
||||||
import {viewRecordToPostView} from '../util/viewRecordToPostView.js'
|
|
||||||
import {Avatar} from './Avatar.js'
|
import {Avatar} from './Avatar.js'
|
||||||
import {Box} from './Box.js'
|
import {Box} from './Box.js'
|
||||||
import * as Grid from './Grid.js'
|
|
||||||
import {CircleInfo} from './icons/CircleInfo.js'
|
|
||||||
import {Heart} from './icons/Heart.js'
|
import {Heart} from './icons/Heart.js'
|
||||||
import {Logomark} from './icons/Logomark.js'
|
import {Logomark} from './icons/Logomark.js'
|
||||||
import {Logotype} from './icons/Logotype.js'
|
import {Logotype} from './icons/Logotype.js'
|
||||||
import {Repost} from './icons/Repost.js'
|
import {Repost} from './icons/Repost.js'
|
||||||
import {Image} from './Image.js'
|
import {PostEmbed} from './PostEmbed.js'
|
||||||
import {RichText} from './RichText.js'
|
import {RichText} from './RichText.js'
|
||||||
import {Text} from './Text.js'
|
import {Text} from './Text.js'
|
||||||
|
|
||||||
@@ -105,7 +85,7 @@ export function Post({
|
|||||||
|
|
||||||
{post.embed && (
|
{post.embed && (
|
||||||
<Box cx={[a.pt_sm]}>
|
<Box cx={[a.pt_sm]}>
|
||||||
<Embeds
|
<PostEmbed
|
||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
data={data}
|
data={data}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
@@ -191,480 +171,3 @@ export function Logo() {
|
|||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Embeds({
|
|
||||||
embed,
|
|
||||||
data,
|
|
||||||
moderation,
|
|
||||||
moderatorData,
|
|
||||||
hideNestedEmbeds,
|
|
||||||
}: {
|
|
||||||
embed: AppBskyFeedDefs.PostView['embed']
|
|
||||||
data: PostData
|
|
||||||
moderation: ModerationDecision
|
|
||||||
moderatorData: ModeratorData
|
|
||||||
hideNestedEmbeds?: boolean
|
|
||||||
}) {
|
|
||||||
/**
|
|
||||||
* If record-with-media, pass through the existing moderation into `Embeds`
|
|
||||||
* and move on to `QuoteEmbed`'s own moderation.
|
|
||||||
*/
|
|
||||||
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
|
||||||
return (
|
|
||||||
<Box cx={[a.gap_md]}>
|
|
||||||
<Embeds
|
|
||||||
embed={embed.media}
|
|
||||||
data={data}
|
|
||||||
moderation={moderation}
|
|
||||||
moderatorData={moderatorData}
|
|
||||||
/>
|
|
||||||
{!hideNestedEmbeds && (
|
|
||||||
<QuoteEmbed
|
|
||||||
embed={embed.record}
|
|
||||||
data={data}
|
|
||||||
moderatorData={moderatorData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mod = moderation.ui('contentMedia')
|
|
||||||
const info = getModerationCauseInfo({
|
|
||||||
cause: mod.blurs.at(0),
|
|
||||||
moderatorData,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (info) {
|
|
||||||
return <ModeratedEmbed info={info} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AppBskyEmbedExternal.isView(embed)) {
|
|
||||||
const {title, description, uri, thumb} = embed.external
|
|
||||||
const image = data.images.get(thumb)
|
|
||||||
return (
|
|
||||||
<LinkCard
|
|
||||||
image={image}
|
|
||||||
title={title}
|
|
||||||
description={description}
|
|
||||||
uri={uri}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AppBskyEmbedImages.isView(embed)) {
|
|
||||||
const {images} = embed
|
|
||||||
|
|
||||||
if (images.length > 0) {
|
|
||||||
const imgs = images.map(({fullsize}) => data.images.get(fullsize))
|
|
||||||
if (imgs.length === 1) {
|
|
||||||
return (
|
|
||||||
<Box cx={[a.rounded_sm, a.w_full, a.overflow_hidden]}>
|
|
||||||
<Image image={imgs[0]} />
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
} else if (imgs.length === 2) {
|
|
||||||
return (
|
|
||||||
<Grid.Row gutter={a.p_xs.padding}>
|
|
||||||
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}>
|
|
||||||
<SquareImage image={imgs[0]} />
|
|
||||||
</Grid.Column>
|
|
||||||
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}>
|
|
||||||
<SquareImage image={imgs[1]} />
|
|
||||||
</Grid.Column>
|
|
||||||
</Grid.Row>
|
|
||||||
)
|
|
||||||
} else if (imgs.length === 3) {
|
|
||||||
return (
|
|
||||||
<Grid.Row gutter={a.p_xs.padding}>
|
|
||||||
<Grid.Column gutter={a.p_xs.padding} width={2 / 3}>
|
|
||||||
<SquareImage image={imgs[0]} />
|
|
||||||
</Grid.Column>
|
|
||||||
<Grid.Column gutter={a.p_xs.padding} width={1 / 3} cx={[a.gap_sm]}>
|
|
||||||
<SquareImage image={imgs[1]} />
|
|
||||||
<SquareImage image={imgs[2]} />
|
|
||||||
</Grid.Column>
|
|
||||||
</Grid.Row>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Grid.Row gutter={a.p_xs.padding}>
|
|
||||||
<Grid.Column gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}>
|
|
||||||
<SquareImage image={imgs[0]} />
|
|
||||||
<SquareImage image={imgs[1]} />
|
|
||||||
</Grid.Column>
|
|
||||||
<Grid.Column gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}>
|
|
||||||
<SquareImage image={imgs[2]} />
|
|
||||||
<SquareImage image={imgs[3]} />
|
|
||||||
</Grid.Column>
|
|
||||||
</Grid.Row>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AppBskyEmbedRecord.isView(embed)) {
|
|
||||||
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
|
|
||||||
return (
|
|
||||||
<FeedCard
|
|
||||||
embed={embed.record}
|
|
||||||
data={data}
|
|
||||||
moderatorData={moderatorData}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AppBskyGraphDefs.isListView(embed.record)) {
|
|
||||||
return (
|
|
||||||
<ListCard
|
|
||||||
embed={embed.record}
|
|
||||||
data={data}
|
|
||||||
moderatorData={moderatorData}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
AppBskyGraphDefs.isValidStarterPackViewBasic(embed.record) &&
|
|
||||||
AppBskyGraphStarterpack.isValidRecord(embed.record.record)
|
|
||||||
) {
|
|
||||||
const uri = getStarterPackImageUri(embed.record)
|
|
||||||
const {name, description} = embed.record.record
|
|
||||||
const image = data.images.get(uri)
|
|
||||||
return <LinkCard image={image} title={name} description={description} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QuoteEmbed embed={embed} data={data} moderatorData={moderatorData} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FeedCard({
|
|
||||||
embed,
|
|
||||||
data,
|
|
||||||
moderatorData,
|
|
||||||
}: {
|
|
||||||
embed: AppBskyFeedDefs.GeneratorView
|
|
||||||
data: PostData
|
|
||||||
moderatorData: ModeratorData
|
|
||||||
}) {
|
|
||||||
const feedModeration = moderateFeedGenerator(
|
|
||||||
embed,
|
|
||||||
moderatorData.moderationOptions,
|
|
||||||
)
|
|
||||||
const modui = feedModeration.ui('contentList')
|
|
||||||
const info = getModerationCauseInfo({
|
|
||||||
cause: modui.blurs.at(0),
|
|
||||||
moderatorData,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (info) {
|
|
||||||
return <ModeratedEmbed info={info} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const {avatar, displayName, likeCount, creator} = embed
|
|
||||||
const image = data.images.get(avatar)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
cx={[
|
|
||||||
a.w_full,
|
|
||||||
a.gap_sm,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.p_md,
|
|
||||||
a.border,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
]}>
|
|
||||||
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
{image && (
|
|
||||||
<Image
|
|
||||||
image={image}
|
|
||||||
cx={[
|
|
||||||
a.rounded_sm,
|
|
||||||
{
|
|
||||||
width: '40px',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Box cx={[a.pt_2xs]}>
|
|
||||||
<Text cx={[a.text_md, a.font_bold, a.pb_2xs]}>{displayName}</Text>
|
|
||||||
<Text cx={[a.text_sm, a.leading_snug]}>By @{creator.handle}</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{likeCount > 1 && (
|
|
||||||
<Text cx={[a.text_sm, a.leading_snug]}>
|
|
||||||
Liked by {formatCount(likeCount)} users
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ListCard({
|
|
||||||
embed,
|
|
||||||
data,
|
|
||||||
moderatorData,
|
|
||||||
}: {
|
|
||||||
embed: AppBskyGraphDefs.ListView
|
|
||||||
data: PostData
|
|
||||||
moderatorData: ModeratorData
|
|
||||||
}) {
|
|
||||||
const listModeration = moderateUserList(
|
|
||||||
embed,
|
|
||||||
moderatorData.moderationOptions,
|
|
||||||
)
|
|
||||||
const modui = listModeration.ui('contentList')
|
|
||||||
const info = getModerationCauseInfo({
|
|
||||||
cause: modui.blurs.at(0),
|
|
||||||
moderatorData,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (info) {
|
|
||||||
return <ModeratedEmbed info={info} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const {avatar, name, creator} = embed
|
|
||||||
const image = data.images.get(avatar)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
cx={[
|
|
||||||
a.w_full,
|
|
||||||
a.gap_sm,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.p_md,
|
|
||||||
a.border,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
]}>
|
|
||||||
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
|
|
||||||
{image && (
|
|
||||||
<Image
|
|
||||||
image={image}
|
|
||||||
cx={[
|
|
||||||
a.rounded_sm,
|
|
||||||
{
|
|
||||||
width: '40px',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Box cx={[a.pt_2xs]}>
|
|
||||||
<Text cx={[a.text_md, a.font_bold, a.pb_2xs]}>{name}</Text>
|
|
||||||
<Text cx={[a.text_sm, a.leading_snug]}>By @{creator.handle}</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LinkCard({
|
|
||||||
image,
|
|
||||||
uri,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
}: {
|
|
||||||
image?: ImageSource
|
|
||||||
uri?: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
cx={[
|
|
||||||
a.w_full,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.overflow_hidden,
|
|
||||||
a.border,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
]}>
|
|
||||||
{image && (
|
|
||||||
<Box
|
|
||||||
cx={[
|
|
||||||
a.relative,
|
|
||||||
a.w_full,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
{paddingTop: (630 / 1200) * 100 + '%'},
|
|
||||||
]}>
|
|
||||||
<Image
|
|
||||||
image={image}
|
|
||||||
cx={[
|
|
||||||
a.absolute,
|
|
||||||
a.inset_0,
|
|
||||||
{
|
|
||||||
objectFit: 'cover',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Box cx={[a.p_md, t.atoms.bg]}>
|
|
||||||
{uri && (
|
|
||||||
<Text cx={[a.text_xs, a.pb_sm, t.atoms.text_contrast_medium]}>
|
|
||||||
{toShortUrl(uri)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<Text cx={[a.text_md, a.font_bold, a.pb_xs]}>{title}</Text>
|
|
||||||
<Text cx={[a.text_sm, a.leading_snug]}>{description}</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SquareImage({image}: {image: ImageSource}) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
cx={[
|
|
||||||
a.relative,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.overflow_hidden,
|
|
||||||
a.w_full,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
{paddingTop: '100%'},
|
|
||||||
]}>
|
|
||||||
<Image
|
|
||||||
image={image}
|
|
||||||
cx={[
|
|
||||||
a.absolute,
|
|
||||||
a.inset_0,
|
|
||||||
{
|
|
||||||
objectFit: 'cover',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function QuoteEmbed({
|
|
||||||
embed,
|
|
||||||
data,
|
|
||||||
moderatorData,
|
|
||||||
}: {
|
|
||||||
embed: AppBskyEmbedRecord.View
|
|
||||||
data: PostData
|
|
||||||
moderatorData: ModeratorData
|
|
||||||
}) {
|
|
||||||
if (
|
|
||||||
AppBskyEmbedRecord.isValidViewRecord(embed.record) &&
|
|
||||||
AppBskyFeedPost.isValidRecord(embed.record.value)
|
|
||||||
) {
|
|
||||||
const {author, value: post, embeds} = embed.record
|
|
||||||
const avatar = data.images.get(author.avatar)
|
|
||||||
const rt = post.text
|
|
||||||
? new RichTextApi({
|
|
||||||
text: post.text,
|
|
||||||
facets: post.facets,
|
|
||||||
})
|
|
||||||
: undefined
|
|
||||||
const postView = viewRecordToPostView(embed.record)
|
|
||||||
const moderation = moderatePost(postView, moderatorData.moderationOptions)
|
|
||||||
|
|
||||||
const mod = moderation.ui('contentView')
|
|
||||||
const info = getModerationCauseInfo({
|
|
||||||
cause: mod.blurs.at(0),
|
|
||||||
moderatorData,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (info) {
|
|
||||||
return <ModeratedEmbed info={info} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
cx={[
|
|
||||||
a.w_full,
|
|
||||||
a.p_md,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.border,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
]}>
|
|
||||||
<Box cx={[a.flex_row, a.align_center, a.gap_xs, a.pb_sm]}>
|
|
||||||
<Avatar
|
|
||||||
size={20}
|
|
||||||
image={avatar}
|
|
||||||
profile={author}
|
|
||||||
moderatorData={moderatorData}
|
|
||||||
/>
|
|
||||||
<Box cx={[a.flex_row, a.align_center, a.gap_xs]}>
|
|
||||||
<Text cx={[a.text_sm, a.font_bold]}>
|
|
||||||
{author.displayName || author.handle}
|
|
||||||
</Text>
|
|
||||||
<Text cx={[a.text_xs, t.atoms.text_contrast_medium]}>
|
|
||||||
@{author.handle}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{rt && <RichText value={rt} cx={[a.text_sm]} />}
|
|
||||||
|
|
||||||
{Boolean(embeds && embeds.length) && (
|
|
||||||
<Box cx={[a.pt_sm]}>
|
|
||||||
<Embeds
|
|
||||||
embed={embeds[0]}
|
|
||||||
data={data}
|
|
||||||
moderation={moderation}
|
|
||||||
moderatorData={moderatorData}
|
|
||||||
hideNestedEmbeds
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
} else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) {
|
|
||||||
return <NotQuoteEmbed>Quoted post is blocked</NotQuoteEmbed>
|
|
||||||
} else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) {
|
|
||||||
return (
|
|
||||||
<NotQuoteEmbed>
|
|
||||||
Quoted post not found, it may have been deleted.
|
|
||||||
</NotQuoteEmbed>
|
|
||||||
)
|
|
||||||
} else if (AppBskyEmbedRecord.isViewDetached(embed.record)) {
|
|
||||||
return <NotQuoteEmbed>Quoted post detached by author</NotQuoteEmbed>
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NotQuoteEmbed({children}: {children: React.ReactNode}) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
cx={[
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_sm,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.p_md,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<CircleInfo size={20} fill={t.atoms.text_contrast_low.color} />
|
|
||||||
<Text cx={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ModeratedEmbed({info}: {info: ModerationCauseInfo}) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
cx={[
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_sm,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.p_md,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<info.icon size={20} fill={t.atoms.text_contrast_low.color} />
|
|
||||||
<Box cx={[a.gap_xs]}>
|
|
||||||
<Text cx={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
|
||||||
{info.name}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import {
|
||||||
|
AppBskyFeedDefs,
|
||||||
|
AppBskyGraphStarterpack,
|
||||||
|
ModerationDecision,
|
||||||
|
} from '@atproto/api'
|
||||||
|
|
||||||
|
import {ModeratorData} from '../data/getModeratorData.js'
|
||||||
|
import {Image as ImageSource, PostData} from '../data/getPostData.js'
|
||||||
|
import {atoms as a} from '../theme/index.js'
|
||||||
|
import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js'
|
||||||
|
import {Embed, EmbedType,parseEmbed} from '../util/parseEmbed.js'
|
||||||
|
import {Box} from './Box.js'
|
||||||
|
import {FeedCard} from './FeedCard.js'
|
||||||
|
import * as Grid from './Grid.js'
|
||||||
|
import {Image, SquareImage} from './Image.js'
|
||||||
|
import {LinkCard} from './LinkCard.js'
|
||||||
|
import {ListCard} from './ListCard.js'
|
||||||
|
import {NotQuotePost,QuotePost} from './PostEmbed/QuotePost.js'
|
||||||
|
|
||||||
|
type CommonProps = {
|
||||||
|
data: PostData
|
||||||
|
moderation: ModerationDecision
|
||||||
|
moderatorData: ModeratorData
|
||||||
|
hideNestedEmbeds?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostEmbed({
|
||||||
|
embed: rawEmbed,
|
||||||
|
...rest
|
||||||
|
}: CommonProps & {
|
||||||
|
embed: AppBskyFeedDefs.PostView['embed']
|
||||||
|
}) {
|
||||||
|
const embed = parseEmbed(rawEmbed)
|
||||||
|
|
||||||
|
switch (embed.type) {
|
||||||
|
case 'images':
|
||||||
|
case 'link':
|
||||||
|
case 'video':
|
||||||
|
return <MediaEmbeds embed={embed} {...rest} />
|
||||||
|
case 'feed':
|
||||||
|
case 'list':
|
||||||
|
case 'starter_pack':
|
||||||
|
case 'post':
|
||||||
|
case 'post_blocked':
|
||||||
|
case 'post_detached':
|
||||||
|
case 'post_not_found':
|
||||||
|
return <RecordEmbeds embed={embed} {...rest} />
|
||||||
|
case 'post_with_media':
|
||||||
|
return (
|
||||||
|
<Box cx={[a.gap_md]}>
|
||||||
|
<MediaEmbeds embed={embed.media} {...rest} />
|
||||||
|
{!rest.hideNestedEmbeds && (
|
||||||
|
<RecordEmbeds embed={embed.view} {...rest} hideNestedEmbeds />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaEmbeds({
|
||||||
|
embed,
|
||||||
|
...rest
|
||||||
|
}: CommonProps & {
|
||||||
|
embed: Embed
|
||||||
|
}) {
|
||||||
|
switch (embed.type) {
|
||||||
|
case 'images': {
|
||||||
|
return <ImagesEmbed embed={embed} {...rest} />
|
||||||
|
}
|
||||||
|
case 'link': {
|
||||||
|
return <LinkEmbed embed={embed} {...rest} />
|
||||||
|
}
|
||||||
|
case 'video': {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecordEmbeds({
|
||||||
|
embed,
|
||||||
|
...rest
|
||||||
|
}: CommonProps & {
|
||||||
|
embed: Embed
|
||||||
|
}) {
|
||||||
|
switch (embed.type) {
|
||||||
|
case 'feed': {
|
||||||
|
return <FeedEmbed embed={embed} {...rest} />
|
||||||
|
}
|
||||||
|
case 'list': {
|
||||||
|
return <ListEmbed embed={embed} {...rest} />
|
||||||
|
}
|
||||||
|
case 'starter_pack': {
|
||||||
|
return <StarterPackEmbed embed={embed} {...rest} />
|
||||||
|
}
|
||||||
|
case 'post': {
|
||||||
|
return <QuoteEmbed embed={embed} {...rest} />
|
||||||
|
}
|
||||||
|
case 'post_blocked': {
|
||||||
|
return <NotQuotePost>Quoted post is blocked</NotQuotePost>
|
||||||
|
}
|
||||||
|
case 'post_detached': {
|
||||||
|
return <NotQuotePost>Quoted post detached by author</NotQuotePost>
|
||||||
|
}
|
||||||
|
case 'post_not_found': {
|
||||||
|
return (
|
||||||
|
<NotQuotePost>
|
||||||
|
Quoted post not found, it may have been deleted.
|
||||||
|
</NotQuotePost>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuoteEmbed({
|
||||||
|
embed,
|
||||||
|
...rest
|
||||||
|
}: CommonProps & {
|
||||||
|
embed: EmbedType<'post'>
|
||||||
|
}) {
|
||||||
|
return <QuotePost embed={embed.view} {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StarterPackEmbed({
|
||||||
|
embed,
|
||||||
|
...rest
|
||||||
|
}: CommonProps & {
|
||||||
|
embed: EmbedType<'starter_pack'>
|
||||||
|
}) {
|
||||||
|
const uri = getStarterPackImageUri(embed.view)
|
||||||
|
if (!AppBskyGraphStarterpack.isValidRecord(embed.view.record)) return null
|
||||||
|
const {name, description} = embed.view.record
|
||||||
|
const image = rest.data.images.get(uri)
|
||||||
|
return <LinkCard image={image} title={name} description={description} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListEmbed({
|
||||||
|
embed,
|
||||||
|
...rest
|
||||||
|
}: CommonProps & {
|
||||||
|
embed: EmbedType<'list'>
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ListCard
|
||||||
|
embed={embed.view}
|
||||||
|
data={rest.data}
|
||||||
|
moderatorData={rest.moderatorData}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeedEmbed({
|
||||||
|
embed,
|
||||||
|
...rest
|
||||||
|
}: CommonProps & {
|
||||||
|
embed: EmbedType<'feed'>
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FeedCard
|
||||||
|
embed={embed.view}
|
||||||
|
data={rest.data}
|
||||||
|
moderatorData={rest.moderatorData}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LinkEmbed({
|
||||||
|
embed,
|
||||||
|
...rest
|
||||||
|
}: CommonProps & {
|
||||||
|
embed: EmbedType<'link'>
|
||||||
|
}) {
|
||||||
|
const {title, description, uri, thumb} = embed.view.external
|
||||||
|
if (!thumb) return null
|
||||||
|
const image = rest.data.images.get(thumb)
|
||||||
|
return (
|
||||||
|
<LinkCard image={image} title={title} description={description} uri={uri} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImagesEmbed({
|
||||||
|
embed,
|
||||||
|
...rest
|
||||||
|
}: CommonProps & {
|
||||||
|
embed: EmbedType<'images'>
|
||||||
|
}) {
|
||||||
|
const {images} = embed.view
|
||||||
|
|
||||||
|
if (images.length > 0) {
|
||||||
|
const imgs = images
|
||||||
|
.map(({fullsize}) => rest.data.images.get(fullsize))
|
||||||
|
.filter(Boolean) as ImageSource[]
|
||||||
|
if (imgs.length === 1) {
|
||||||
|
return (
|
||||||
|
<Box cx={[a.rounded_sm, a.w_full, a.overflow_hidden]}>
|
||||||
|
<Image image={imgs[0]} />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} else if (imgs.length === 2) {
|
||||||
|
return (
|
||||||
|
<Grid.Row gutter={a.p_xs.padding}>
|
||||||
|
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}>
|
||||||
|
<SquareImage image={imgs[0]} />
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column gutter={a.p_xs.padding} width={1 / 2}>
|
||||||
|
<SquareImage image={imgs[1]} />
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
)
|
||||||
|
} else if (imgs.length === 3) {
|
||||||
|
return (
|
||||||
|
<Grid.Row gutter={a.p_xs.padding}>
|
||||||
|
<Grid.Column gutter={a.p_xs.padding} width={2 / 3}>
|
||||||
|
<SquareImage image={imgs[0]} />
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column gutter={a.p_xs.padding} width={1 / 3} cx={[a.gap_sm]}>
|
||||||
|
<SquareImage image={imgs[1]} />
|
||||||
|
<SquareImage image={imgs[2]} />
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Grid.Row gutter={a.p_xs.padding}>
|
||||||
|
<Grid.Column gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}>
|
||||||
|
<SquareImage image={imgs[0]} />
|
||||||
|
<SquareImage image={imgs[1]} />
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column gutter={a.p_xs.padding} width={1 / 2} cx={[a.gap_sm]}>
|
||||||
|
<SquareImage image={imgs[2]} />
|
||||||
|
<SquareImage image={imgs[3]} />
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
AppBskyEmbedRecord,
|
||||||
|
AppBskyFeedPost,
|
||||||
|
RichText as RichTextApi,
|
||||||
|
} from '@atproto/api'
|
||||||
|
|
||||||
|
import {ModeratorData} from '../../data/getModeratorData.js'
|
||||||
|
import {PostData} from '../../data/getPostData.js'
|
||||||
|
import {atoms as a, theme as t} from '../../theme/index.js'
|
||||||
|
import {getModerationCauseInfo} from '../../util/getModerationCauseInfo.js'
|
||||||
|
import {moderatePost} from '../../util/moderatePost.js'
|
||||||
|
import {viewRecordToPostView} from '../../util/viewRecordToPostView.js'
|
||||||
|
import {Avatar} from '../Avatar.js'
|
||||||
|
import {Box} from '../Box.js'
|
||||||
|
import {CircleInfo} from '../icons/CircleInfo.js'
|
||||||
|
import {ModeratedEmbed} from '../ModeratedEmbed.js'
|
||||||
|
import {PostEmbed} from '../PostEmbed.js'
|
||||||
|
import {RichText} from '../RichText.js'
|
||||||
|
import {Text} from '../Text.js'
|
||||||
|
|
||||||
|
export function QuotePost({
|
||||||
|
embed,
|
||||||
|
data,
|
||||||
|
moderatorData,
|
||||||
|
}: {
|
||||||
|
embed: AppBskyEmbedRecord.ViewRecord
|
||||||
|
data: PostData
|
||||||
|
moderatorData: ModeratorData
|
||||||
|
}) {
|
||||||
|
const {author, value: post, embeds} = embed
|
||||||
|
const avatar = data.images.get(author.avatar || '')
|
||||||
|
const rt =
|
||||||
|
AppBskyFeedPost.isValidRecord(post) && post.text
|
||||||
|
? new RichTextApi({
|
||||||
|
text: post.text,
|
||||||
|
facets: post.facets,
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
const postView = viewRecordToPostView(embed)
|
||||||
|
const moderation = moderatePost(postView, moderatorData.moderationOptions)
|
||||||
|
|
||||||
|
const mod = moderation.ui('contentView')
|
||||||
|
const info = getModerationCauseInfo({
|
||||||
|
cause: mod.blurs.at(0),
|
||||||
|
moderatorData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
return <ModeratedEmbed info={info} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
cx={[
|
||||||
|
a.w_full,
|
||||||
|
a.p_md,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<Box cx={[a.flex_row, a.align_center, a.gap_xs, a.pb_sm]}>
|
||||||
|
<Avatar
|
||||||
|
size={20}
|
||||||
|
image={avatar}
|
||||||
|
profile={author}
|
||||||
|
moderatorData={moderatorData}
|
||||||
|
/>
|
||||||
|
<Box cx={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
<Text cx={[a.text_sm, a.font_bold]}>
|
||||||
|
{author.displayName || author.handle}
|
||||||
|
</Text>
|
||||||
|
<Text cx={[a.text_xs, t.atoms.text_contrast_medium]}>
|
||||||
|
@{author.handle}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{rt && <RichText value={rt} cx={[a.text_sm]} />}
|
||||||
|
|
||||||
|
{embeds && embeds.length && (
|
||||||
|
<Box cx={[a.pt_sm]}>
|
||||||
|
<PostEmbed
|
||||||
|
embed={embeds[0]}
|
||||||
|
data={data}
|
||||||
|
moderation={moderation}
|
||||||
|
moderatorData={moderatorData}
|
||||||
|
hideNestedEmbeds
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotQuotePost({children}: {children: React.ReactNode}) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
cx={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.p_md,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<CircleInfo size={20} fill={t.atoms.text_contrast_low.color} />
|
||||||
|
<Text cx={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import {AppBskyFeedDefs} from '@atproto/api'
|
|||||||
|
|
||||||
import {httpLogger} from '../logger.js'
|
import {httpLogger} from '../logger.js'
|
||||||
import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js'
|
import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js'
|
||||||
import {Embed,parseEmbed} from '../util/parseEmbed.js'
|
import {Embed, parseEmbed} from '../util/parseEmbed.js'
|
||||||
import {getImage} from './getImage.js'
|
import {getImage} from './getImage.js'
|
||||||
|
|
||||||
export type Metadata = {
|
export type Metadata = {
|
||||||
@@ -103,9 +103,11 @@ export function getEmbedData(embed: Embed, images: Map<string, Metadata>) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (embed.view.embeds) {
|
||||||
for (const _e of embed.view.embeds) {
|
for (const _e of embed.view.embeds) {
|
||||||
getEmbedData(parseEmbed(_e), images)
|
getEmbedData(parseEmbed(_e), images)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'post_with_media': {
|
case 'post_with_media': {
|
||||||
@@ -125,6 +127,7 @@ export async function getPostData(
|
|||||||
post: AppBskyFeedDefs.PostView,
|
post: AppBskyFeedDefs.PostView,
|
||||||
): Promise<PostData> {
|
): Promise<PostData> {
|
||||||
const images: Map<string, Metadata> = new Map()
|
const images: Map<string, Metadata> = new Map()
|
||||||
|
console.log(JSON.stringify(post, null, 2))
|
||||||
|
|
||||||
if (post.author.avatar) {
|
if (post.author.avatar) {
|
||||||
images.set(post.author.avatar, {
|
images.set(post.author.avatar, {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function getModerationCauseInfo({
|
|||||||
cause,
|
cause,
|
||||||
moderatorData,
|
moderatorData,
|
||||||
}: {
|
}: {
|
||||||
cause: ModerationCause
|
cause?: ModerationCause
|
||||||
moderatorData: ModeratorData
|
moderatorData: ModeratorData
|
||||||
}): ModerationCauseInfo | undefined {
|
}): ModerationCauseInfo | undefined {
|
||||||
if (!cause) return undefined
|
if (!cause) return undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user