Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc0dfa6cab | |||
| a05c90ae29 | |||
| 196f761cf8 | |||
| 7ab9df946e | |||
| 777ae21051 | |||
| 0956a40f7a | |||
| 9fcd10be32 | |||
| 55566dfcae | |||
| 4c9bf824bd | |||
| d7cef8eb51 | |||
| ccde215545 | |||
| e78fa95c3e | |||
| 25b56c40d2 | |||
| a827da385c | |||
| f6cc7a74d8 | |||
| cfc6ea86e3 | |||
| 450c6bcda4 | |||
| 1ce2325488 | |||
| 462ae8736c | |||
| 8e2118095a | |||
| 25649bd718 | |||
| 75ff59e438 | |||
| c6c9c45819 | |||
| cc86d596a4 | |||
| c959e11376 | |||
| 9a8b2f9e3a | |||
| 116fef81e6 | |||
| 110c74c610 | |||
| 223ba68a0b | |||
| 5a4e75238f | |||
| 47101bfe2c | |||
| 023dbc3032 | |||
| d74c1678b2 | |||
| 775d837f8c | |||
| 71aa7e2fd2 | |||
| cd2ddd21ef | |||
| 0a364f6957 | |||
| 6574157c66 | |||
| 95aebf5724 | |||
| 9fcd83e06f | |||
| 97fcc42c57 | |||
| c4de2e8986 | |||
| 998f4038b0 | |||
| d15585bea4 | |||
| e74bd093ee |
@@ -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,2 @@
|
||||
700-Noto*
|
||||
400-Noto*
|
||||
+10
-5
@@ -4,24 +4,29 @@
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "node --loader ts-node/esm ./src/bin.ts",
|
||||
"dev": "node --watch-path ./src --loader ts-node/esm ./src/bin.ts",
|
||||
"start": " NODE_ENV=production node --loader ts-node/esm ./src/bin.ts",
|
||||
"dev": "LOG_ENABLED=true NODE_ENV=development node --watch-path ./src --loader ts-node/esm ./src/bin.ts | pino-pretty",
|
||||
"build": "tsc && cp -r src/assets dist/",
|
||||
"install-fonts": "node --loader ts-node/esm scripts/install-fonts.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.12.19-next.0",
|
||||
"@atproto/common": "^0.4.0",
|
||||
"@atproto/api": "^0.15.14",
|
||||
"@atproto/common": "^0.4.11",
|
||||
"@bsky.app/alf": "^0.1.0",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"express": "^4.19.2",
|
||||
"http-terminator": "^3.2.0",
|
||||
"node-vibrant": "^4.0.3",
|
||||
"pino": "^9.2.0",
|
||||
"react": "^18.3.1",
|
||||
"satori": "^0.10.13",
|
||||
"twemoji": "^14.0.2"
|
||||
"twemoji": "^14.0.2",
|
||||
"whatwg-mimetype": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.3",
|
||||
"pino-pretty": "^11.2.2",
|
||||
"prettier": "^3.5.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
|
||||
@@ -2,36 +2,42 @@ import {writeFile} from 'node:fs/promises'
|
||||
import * as path from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
|
||||
const __DIRNAME = path.dirname(fileURLToPath(import.meta.url))
|
||||
import {
|
||||
ADDITIONAL_FONTS,
|
||||
ADDITIONAL_FONTS_POSTSCRIPT_NAMES,
|
||||
} from '../src/util/fonts.js'
|
||||
|
||||
const FONTS = [
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-jp@5.0/japanese-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-tc@5.0/chinese-traditional-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-sc@5.0/chinese-simplified-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hk@5.0/chinese-hongkong-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-kr@5.0/korean-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-thai@5.0/thai-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-arabic@5.0/arabic-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hebrew@5.0/hebrew-700-normal.ttf',
|
||||
]
|
||||
const __DIRNAME = path.dirname(fileURLToPath(import.meta.url))
|
||||
const outDir = path.join(__DIRNAME, '..', 'src', 'assets', 'fonts')
|
||||
|
||||
async function main() {
|
||||
await Promise.all(
|
||||
FONTS.map(async urlStr => {
|
||||
const url = new URL(urlStr)
|
||||
const res = await fetch(url)
|
||||
const font = await res.arrayBuffer()
|
||||
const filename = url.pathname
|
||||
.split('/')
|
||||
.slice(-2)
|
||||
.join('/')
|
||||
.replace(/@[\d.]+\//, '-')
|
||||
ADDITIONAL_FONTS.map(async urlStr => {
|
||||
// get last part of the URL
|
||||
const raw = urlStr.split('/').slice(-1).join('')
|
||||
// style and weight are known parts of the path, rest is the font family
|
||||
const [styleWithExtension, weight, ...fontFamilyParts] = raw
|
||||
.split('-')
|
||||
.reverse()
|
||||
const style = styleWithExtension.split('.')[0]
|
||||
// re-reverse the font family parts to get the original order
|
||||
const rawFontFamily = fontFamilyParts.reverse().join('-')
|
||||
// get the postscript name from the map
|
||||
const postScriptName = ADDITIONAL_FONTS_POSTSCRIPT_NAMES[rawFontFamily]
|
||||
// our filename format: `weight-postScriptName-style.ttf`
|
||||
const filename = `${weight}-${postScriptName}-${style}.ttf`
|
||||
// fetch the file
|
||||
const res = await fetch(urlStr)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: fetching failed for ${filename}`)
|
||||
throw new Error(`HTTP ${res.status}: fetching failed for ${urlStr}`)
|
||||
}
|
||||
|
||||
console.log(`Writing font ${filename}...`)
|
||||
|
||||
await writeFile(
|
||||
path.join(__DIRNAME, '..', 'src', 'assets', 'fonts', filename),
|
||||
Buffer.from(font),
|
||||
path.join(outDir, filename),
|
||||
Buffer.from(await res.arrayBuffer()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,7 +9,7 @@ async function main() {
|
||||
const cfg = envToCfg(env)
|
||||
const card = await CardService.create(cfg)
|
||||
await card.start()
|
||||
httpLogger.info('card service is running')
|
||||
httpLogger.info(`card service is running on port ${cfg.service.port}`)
|
||||
process.on('SIGTERM', async () => {
|
||||
httpLogger.info('card service is stopping')
|
||||
await card.destroy()
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||
|
||||
import {ModeratorData} from '../data/getModeratorData.js'
|
||||
import {Image as ImageSource} from '../data/getPostData.js'
|
||||
import {atoms as a, theme as t} from '../theme/index.js'
|
||||
import {Box} from './Box.js'
|
||||
import {DefaultUser} from './icons/avatars/DefaultUser.js'
|
||||
import {Image} from './Image.js'
|
||||
|
||||
export function Avatar({
|
||||
size,
|
||||
image,
|
||||
profile,
|
||||
moderatorData,
|
||||
}: {
|
||||
size: number
|
||||
image: ImageSource
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
moderatorData: ModeratorData
|
||||
}) {
|
||||
const moderation = moderateProfile(profile, moderatorData.moderationOptions)
|
||||
const modui = moderation.ui('avatar')
|
||||
const blur = !!modui?.blurs[0]
|
||||
const isLabeller = !!profile.associated?.labeler
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
isLabeller ? a.rounded_xs : a.rounded_full,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
width: size + 'px',
|
||||
height: size + 'px',
|
||||
},
|
||||
blur && {
|
||||
filter: 'blur(2.5px)',
|
||||
},
|
||||
]}>
|
||||
{image ? (
|
||||
<Image height="100%" width="100%" image={image} />
|
||||
) : (
|
||||
<DefaultUser size={size} />
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {atoms as a, style as s} from '../theme/index.js'
|
||||
|
||||
export function Box({
|
||||
children,
|
||||
cx = [],
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
cx?: (Record<string, any> | undefined)[]
|
||||
}) {
|
||||
return <div style={s([a.flex, a.flex_col, ...cx])}>{children}</div>
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react'
|
||||
|
||||
export function Butterfly(props: React.SVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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 {sanitizeHandle} from '../util/sanitizeHandle.js'
|
||||
import {Box} from './Box.js'
|
||||
import {DefaultFeed} from './icons/avatars/DefaultFeed.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',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<DefaultFeed size={40} />
|
||||
)}
|
||||
<Box cx={[a.pt_2xs, a.flex_1]}>
|
||||
<Text cx={[a.text_md, a.font_bold, a.pb_2xs, a.line_clamp_1]}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text
|
||||
cx={[
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.line_clamp_1,
|
||||
]}>
|
||||
{`By ${sanitizeHandle(creator.handle, '@')}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{likeCount > 1 && (
|
||||
<Text
|
||||
cx={[
|
||||
a.text_xs,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.font_bold,
|
||||
a.leading_snug,
|
||||
]}>
|
||||
Liked by {formatCount(likeCount)} users
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export function Globe(props: React.SVGAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M4.4 9.493C4.14 10.28 4 11.124 4 12a8 8 0 1 0 10.899-7.459l-.953 3.81a1 1 0 0 1-.726.727l-3.444.866-.772 1.533a1 1 0 0 1-1.493.35L4.4 9.493Zm.883-1.84L7.756 9.51l.44-.874a1 1 0 0 1 .649-.52l3.306-.832.807-3.227a7.99 7.99 0 0 0-7.676 3.597ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8.43.162a1 1 0 0 1 .77-.29l1.89.121a1 1 0 0 1 .494.168l2.869 1.928a1 1 0 0 1 .336 1.277l-.973 1.946a1 1 0 0 1-.894.553h-2.92a1 1 0 0 1-.831-.445L9.225 14.5a1 1 0 0 1 .126-1.262l1.08-1.076Zm.915 1.913.177-.177 1.171.074 1.914 1.286-.303.607h-1.766l-1.194-1.79Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {atoms as a} from '../theme/index.js'
|
||||
import {Box} from './Box.js'
|
||||
|
||||
export function Row({
|
||||
children,
|
||||
gutter = 0,
|
||||
cx,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
gutter?: number
|
||||
cx?: Record<string, any>[]
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.flex_row,
|
||||
{
|
||||
marginLeft: -gutter,
|
||||
marginRight: -gutter,
|
||||
},
|
||||
...(cx || []),
|
||||
]}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export function Column({
|
||||
children,
|
||||
gutter,
|
||||
cx,
|
||||
width = 1,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
gutter?: number
|
||||
cx?: Record<string, any>[]
|
||||
width?: number
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.flex_col,
|
||||
{
|
||||
paddingLeft: gutter,
|
||||
paddingRight: gutter,
|
||||
width: `${width * 100}%`,
|
||||
},
|
||||
...(cx || []),
|
||||
]}>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {Image as ImageSource} from '../data/getPostData.js'
|
||||
import {atoms as a, style as s, StyleProp, theme as t} from '../theme/index.js'
|
||||
import {Box} from './Box.js'
|
||||
import {MediaInsetBorder} from './MediaInsetBorder.js'
|
||||
|
||||
export type ImageProps = Omit<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
'src' | 'style'
|
||||
> & {
|
||||
image: ImageSource
|
||||
cx?: Record<string, any>[]
|
||||
}
|
||||
|
||||
export function Image({image, cx, ...rest}: ImageProps) {
|
||||
const {image: src, mime = 'image/jpeg'} = image
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
src={`data:${mime};base64,${src.toString('base64')}`}
|
||||
style={cx ? s(cx) : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function SquareImage({
|
||||
image,
|
||||
style,
|
||||
insetBorderStyle,
|
||||
}: {
|
||||
image: ImageSource
|
||||
insetBorderStyle?: StyleProp['style']
|
||||
} & StyleProp) {
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.relative,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
a.w_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
{paddingTop: '100%'},
|
||||
style,
|
||||
]}>
|
||||
<Image
|
||||
image={image}
|
||||
cx={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
{
|
||||
objectFit: 'cover',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<MediaInsetBorder style={insetBorderStyle} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react'
|
||||
|
||||
export function Img(
|
||||
props: Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> & {src: Buffer},
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import {Image as ImageSource} from '../data/getPostData.js'
|
||||
import {atoms as a, style as s, theme as t} from '../theme/index.js'
|
||||
import {toNiceDomain} from '../util/toNiceDomain.js'
|
||||
import {Box} from './Box.js'
|
||||
import {Globe} from './Globe.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.px_md, a.pt_md, t.atoms.bg]}>
|
||||
<Text cx={[a.text_md, a.font_bold, a.pb_xs, a.line_clamp_3]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
cx={[
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
a.pb_sm,
|
||||
image ? a.line_clamp_2 : a.line_clamp_4,
|
||||
]}>
|
||||
{description}
|
||||
</Text>
|
||||
<Box
|
||||
cx={[
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
{paddingTop: 6},
|
||||
a.pb_sm,
|
||||
a.gap_2xs,
|
||||
]}>
|
||||
<Globe height={12} width={12} style={t.atoms.text_contrast_medium} />
|
||||
<Text
|
||||
cx={[
|
||||
a.text_xs,
|
||||
a.pb_sm,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.max_w_full,
|
||||
// I don't know why, but the text element is too tall. manually set the height
|
||||
{overflowX: 'hidden', height: 14},
|
||||
a.line_clamp_1,
|
||||
]}>
|
||||
{toNiceDomain(uri)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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 {sanitizeHandle} from '../util/sanitizeHandle.js'
|
||||
import {Box} from './Box.js'
|
||||
import {DefaultFeed} from './icons/avatars/DefaultFeed.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',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<DefaultFeed size={40} />
|
||||
)}
|
||||
<Box cx={[a.pt_2xs, a.flex_1]}>
|
||||
<Text cx={[a.text_md, a.font_bold, a.pb_2xs, a.line_clamp_1]}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text
|
||||
cx={[
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.line_clamp_1,
|
||||
]}>
|
||||
{`By ${sanitizeHandle(creator.handle, '@')}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {atoms as a, StyleProp, theme as t} from '../theme/index.js'
|
||||
import {Box} from './Box.js'
|
||||
|
||||
/**
|
||||
* Applies and thin border within a bounding box. Used to contrast media from
|
||||
* bg of the container.
|
||||
*
|
||||
* @see `#/components/MediaInsetBorder.tsx`
|
||||
*/
|
||||
export function MediaInsetBorder({
|
||||
children,
|
||||
style,
|
||||
opaque,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
/**
|
||||
* Used where this border needs to match adjacent borders, such as in
|
||||
* external link previews
|
||||
*/
|
||||
opaque?: boolean
|
||||
} & StyleProp) {
|
||||
const isLight = t.name === 'light'
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
opaque
|
||||
? [t.atoms.border_contrast_low]
|
||||
: [
|
||||
isLight
|
||||
? t.atoms.border_contrast_low
|
||||
: t.atoms.border_contrast_high,
|
||||
{opacity: 0.6},
|
||||
],
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
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 * as bsky from '../../types/bsky/index.js'
|
||||
import {formatCount} from '../../util/formatCount.js'
|
||||
import {formatDate} from '../../util/formatDate.js'
|
||||
import {computeMatBorderRadius, MAT_WIDTH} from '../../util/frameMatting.js'
|
||||
import {moderatePost} from '../../util/moderatePost.js'
|
||||
import {sanitizeHandle} from '../../util/sanitizeHandle.js'
|
||||
import {getVerificationState} from '../../util/verificationState.js'
|
||||
import {Avatar} from '../Avatar.js'
|
||||
import {Box} from '../Box.js'
|
||||
import {Heart} from '../icons/Heart.js'
|
||||
import {Logomark} from '../icons/Logomark.js'
|
||||
import {Logotype} from '../icons/Logotype.js'
|
||||
import {Repost} from '../icons/Repost.js'
|
||||
import {PostEmbed} from '../PostEmbed.js'
|
||||
import {RichText} from '../RichText.js'
|
||||
import {Text} from '../Text.js'
|
||||
import {VerificationCheck} from '../VerificationCheck.js'
|
||||
|
||||
export function Post({
|
||||
post,
|
||||
data,
|
||||
moderatorData,
|
||||
displayOptions: opts = {
|
||||
mat: false,
|
||||
},
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
data: PostData
|
||||
moderatorData: ModeratorData
|
||||
displayOptions?: {
|
||||
mat?: boolean
|
||||
}
|
||||
}) {
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
) {
|
||||
const avatar = data.images.get(post.author.avatar)
|
||||
const rt = post.record.text
|
||||
? new RichTextApi({
|
||||
text: post.record.text,
|
||||
facets: post.record.facets,
|
||||
})
|
||||
: undefined
|
||||
const hasInteractions = post.likeCount > 0 || post.repostCount > 0
|
||||
const moderation = moderatePost(post, moderatorData.moderationOptions)
|
||||
const verification = getVerificationState({profile: post.author})
|
||||
|
||||
const bgTop = avatar?.colors?.muted ?? '#1083fe'
|
||||
const bgBottom = avatar?.colors?.lightMuted ?? '#67b0fe'
|
||||
const rounding = a.rounded_md
|
||||
|
||||
return (
|
||||
<Box
|
||||
cx={[
|
||||
a.flex,
|
||||
a.align_center,
|
||||
a.w_full,
|
||||
a.h_full,
|
||||
opts.mat && [
|
||||
{
|
||||
padding: MAT_WIDTH,
|
||||
borderRadius: computeMatBorderRadius(rounding.borderRadius),
|
||||
backgroundImage: `linear-gradient(to bottom, ${bgTop} 0%, ${bgBottom} 100%)`,
|
||||
},
|
||||
],
|
||||
]}>
|
||||
<Box
|
||||
cx={[
|
||||
a.flex,
|
||||
a.flex_col,
|
||||
a.w_full,
|
||||
a.p_xl,
|
||||
rounding,
|
||||
t.atoms.bg,
|
||||
{boxShadow: `0 0 20px rgb(0, 25, 51, 0.2)`},
|
||||
]}>
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_sm, a.pb_sm, a.w_full]}>
|
||||
<Box cx={[a.flex_shrink_0]}>
|
||||
<Avatar
|
||||
size={42}
|
||||
image={avatar}
|
||||
profile={post.author}
|
||||
moderatorData={moderatorData}
|
||||
/>
|
||||
</Box>
|
||||
<Box cx={[a.flex_col, a.flex_1]}>
|
||||
<Box cx={[a.flex_1, a.flex_row]}>
|
||||
<Text cx={[a.text_md, a.font_bold, a.pb_2xs, a.line_clamp_1]}>
|
||||
{post.author.displayName || post.author.handle}
|
||||
</Text>
|
||||
{verification.isVerified && (
|
||||
<Box cx={[a.pl_xs, a.pt_2xs]}>
|
||||
<VerificationCheck
|
||||
size={12}
|
||||
verifier={verification.role === 'verifier'}
|
||||
fill={t.palette.primary_500}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Text
|
||||
cx={[a.text_sm, t.atoms.text_contrast_medium, a.line_clamp_1]}>
|
||||
{sanitizeHandle(post.author.handle, '@')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{rt && <RichText value={rt} />}
|
||||
|
||||
{post.embed && (
|
||||
<PostEmbed
|
||||
embed={post.embed}
|
||||
data={data}
|
||||
moderation={moderation}
|
||||
moderatorData={moderatorData}
|
||||
cx={[a.pt_sm]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box cx={[a.flex_row, a.align_center, a.justify_between, a.pt_md]}>
|
||||
<Text cx={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{formatDate(post.record.createdAt)}
|
||||
</Text>
|
||||
|
||||
{!hasInteractions && <Logo />}
|
||||
</Box>
|
||||
|
||||
{hasInteractions && (
|
||||
<Box cx={[a.pt_md]}>
|
||||
<Box
|
||||
cx={[
|
||||
a.w_full,
|
||||
a.pb_md,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
]}
|
||||
/>
|
||||
|
||||
<Box cx={[a.flex_row, a.align_center, a.justify_between]}>
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_2xl]}>
|
||||
{post.likeCount > 0 && (
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Heart size={22} fill={t.palette.red_400} />
|
||||
<Box
|
||||
cx={[
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{formatCount(post.likeCount)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{post.repostCount > 0 && (
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<Repost size={22} fill={t.palette.green_600} />
|
||||
<Box
|
||||
cx={[
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{formatCount(post.repostCount)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Logo />
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function Logo() {
|
||||
return (
|
||||
<Box cx={[a.flex_row, a.align_center]}>
|
||||
<Logomark size={20} fill={t.palette.primary_500} />
|
||||
<Box
|
||||
cx={[
|
||||
{
|
||||
paddingTop: '3px',
|
||||
paddingLeft: '6px',
|
||||
},
|
||||
]}>
|
||||
<Logotype size={64} fill={t.palette.contrast_800} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
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, theme as t} from '../theme/index.js'
|
||||
import * as bsky from '../types/bsky/index.js'
|
||||
import {formatCount} from '../util/formatCount.js'
|
||||
import {getModerationCauseInfo} from '../util/getModerationCauseInfo.js'
|
||||
import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js'
|
||||
import {Embed, EmbedType, parseEmbed} from '../util/parseEmbed.js'
|
||||
import {parseEmbedPlayerFromUrl} from '../util/parseEmbedPlayerFromUrl.js'
|
||||
import {sanitizeHandle} from '../util/sanitizeHandle.js'
|
||||
import {Box} from './Box.js'
|
||||
import {FeedCard} from './FeedCard.js'
|
||||
import * as Grid from './Grid.js'
|
||||
import {PlayFilled as Play} from './icons/Play.js'
|
||||
import {StarterPack} from './icons/StarterPack.js'
|
||||
import {Image, SquareImage} from './Image.js'
|
||||
import {LinkCard} from './LinkCard.js'
|
||||
import {ListCard} from './ListCard.js'
|
||||
import {ModeratedEmbed} from './ModeratedEmbed.js'
|
||||
import {NotQuotePost, QuotePost} from './PostEmbed/QuotePost.js'
|
||||
import {Text} from './Text.js'
|
||||
|
||||
type CommonProps = {
|
||||
data: PostData
|
||||
moderation: ModerationDecision
|
||||
moderatorData: ModeratorData
|
||||
hideNestedEmbeds?: boolean
|
||||
}
|
||||
|
||||
export function PostEmbed({
|
||||
embed: rawEmbed,
|
||||
cx,
|
||||
...rest
|
||||
}: CommonProps & {
|
||||
embed: AppBskyFeedDefs.PostView['embed']
|
||||
cx?: (Record<string, any> | undefined)[]
|
||||
}) {
|
||||
const embed = parseEmbed(rawEmbed)
|
||||
|
||||
switch (embed.type) {
|
||||
case 'images':
|
||||
case 'link':
|
||||
case 'video':
|
||||
return (
|
||||
<Box cx={cx}>
|
||||
<MediaEmbeds embed={embed} {...rest} />
|
||||
</Box>
|
||||
)
|
||||
case 'feed':
|
||||
case 'list':
|
||||
case 'starter_pack':
|
||||
case 'post':
|
||||
case 'post_blocked':
|
||||
case 'post_detached':
|
||||
case 'post_not_found':
|
||||
return rest.hideNestedEmbeds ? null : (
|
||||
<Box cx={cx}>
|
||||
<RecordEmbeds embed={embed} {...rest} />
|
||||
</Box>
|
||||
)
|
||||
case 'post_with_media':
|
||||
return (
|
||||
<Box cx={[a.gap_md, ...cx]}>
|
||||
<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 <VideoEmbed embed={embed} {...rest} />
|
||||
}
|
||||
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 (
|
||||
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||
embed.view.record,
|
||||
AppBskyGraphStarterpack.isRecord,
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const {name, description} = embed.view.record
|
||||
const image = rest.data.images.get(uri)
|
||||
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.px_md, a.py_sm, t.atoms.bg]}>
|
||||
<Box cx={[a.w_full, a.gap_xs]}>
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_xs, a.w_full]}>
|
||||
<StarterPack size={40} fill={t.palette.primary_500} />
|
||||
<Box cx={[a.flex_1]}>
|
||||
<Text
|
||||
cx={[a.text_sm, a.font_bold, a.leading_snug, a.line_clamp_2]}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text
|
||||
cx={[
|
||||
a.leading_snug,
|
||||
a.text_xs,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.line_clamp_1,
|
||||
]}>
|
||||
{`Starter pack by ${sanitizeHandle(
|
||||
embed.view.creator.handle,
|
||||
'@',
|
||||
)}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{description ? (
|
||||
<Box cx={[a.flex_1]}>
|
||||
<Text cx={[a.leading_snug, a.text_xs, a.line_clamp_3]}>
|
||||
{description}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
{embed.view.joinedAllTimeCount >= 50 && (
|
||||
<Text cx={[a.font_bold, a.text_xs, t.atoms.text_contrast_medium]}>
|
||||
{formatCount(embed.view.joinedAllTimeCount)} users have joined!
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
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 modui = rest.moderation.ui('contentMedia')
|
||||
const info = getModerationCauseInfo({
|
||||
cause: modui.blurs.at(0),
|
||||
moderatorData: rest.moderatorData,
|
||||
})
|
||||
|
||||
if (info) {
|
||||
return <ModeratedEmbed info={info} />
|
||||
}
|
||||
|
||||
const image = rest.data.images.get(thumb)
|
||||
const player = parseEmbedPlayerFromUrl(uri)
|
||||
const gif = rest.data.images.get(player?.playerUri)
|
||||
if (gif) {
|
||||
return (
|
||||
<Box cx={[a.rounded_sm, a.overflow_hidden]}>
|
||||
<Image image={gif} />
|
||||
<Box
|
||||
cx={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
{
|
||||
top: '75%',
|
||||
backgroundImage:
|
||||
'linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.75))',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Box
|
||||
cx={[
|
||||
a.absolute,
|
||||
a.rounded_xs,
|
||||
a.px_xs,
|
||||
a.py_2xs,
|
||||
{
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
bottom: a.p_md.padding,
|
||||
right: a.p_md.padding,
|
||||
},
|
||||
]}>
|
||||
<Text cx={[t.atoms.text, a.font_bold, a.text_sm]}>GIF</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<LinkCard image={image} title={title} description={description} uri={uri} />
|
||||
)
|
||||
}
|
||||
|
||||
export function VideoEmbed({
|
||||
embed,
|
||||
...rest
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'video'>
|
||||
}) {
|
||||
const {thumbnail} = embed.view
|
||||
const modui = rest.moderation.ui('contentMedia')
|
||||
const info = getModerationCauseInfo({
|
||||
cause: modui.blurs.at(0),
|
||||
moderatorData: rest.moderatorData,
|
||||
})
|
||||
|
||||
if (info) {
|
||||
return <ModeratedEmbed info={info} />
|
||||
}
|
||||
|
||||
const img = rest.data.images.get(thumbnail)
|
||||
if (img) {
|
||||
return (
|
||||
<Box cx={[a.relative, a.rounded_sm, a.w_full, a.overflow_hidden]}>
|
||||
<Image image={img} />
|
||||
<Box
|
||||
cx={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.rounded_full,
|
||||
{
|
||||
margin: 'auto',
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
},
|
||||
]}>
|
||||
<Box
|
||||
cx={[
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.rounded_full,
|
||||
{
|
||||
margin: 'auto',
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
opacity: 0.7,
|
||||
},
|
||||
]}>
|
||||
<Play size={32} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function ImagesEmbed({
|
||||
embed,
|
||||
...rest
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'images'>
|
||||
}) {
|
||||
const gutter = a.p_2xs.padding
|
||||
const {images} = embed.view
|
||||
const modui = rest.moderation.ui('contentMedia')
|
||||
const info = getModerationCauseInfo({
|
||||
cause: modui.blurs.at(0),
|
||||
moderatorData: rest.moderatorData,
|
||||
})
|
||||
|
||||
if (info) {
|
||||
return <ModeratedEmbed info={info} />
|
||||
}
|
||||
|
||||
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={gutter}>
|
||||
<Grid.Column width={1 / 2} gutter={gutter}>
|
||||
<SquareImage
|
||||
image={imgs[0]}
|
||||
style={{
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
insetBorderStyle={{
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
/>
|
||||
</Grid.Column>
|
||||
<Grid.Column width={1 / 2} gutter={gutter}>
|
||||
<SquareImage
|
||||
image={imgs[1]}
|
||||
style={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}
|
||||
insetBorderStyle={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
)
|
||||
} else if (imgs.length === 3) {
|
||||
return (
|
||||
<Grid.Row gutter={gutter}>
|
||||
<Grid.Column gutter={gutter} width={1 / 2}>
|
||||
<SquareImage
|
||||
image={imgs[0]}
|
||||
style={{
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
insetBorderStyle={{
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
/>
|
||||
</Grid.Column>
|
||||
<Grid.Column gutter={gutter} width={1 / 2} cx={[a.gap_xs]}>
|
||||
<SquareImage
|
||||
image={imgs[1]}
|
||||
style={{
|
||||
// awkward number found through trial and error to account for gap
|
||||
paddingTop: '48.75%',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
insetBorderStyle={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
/>
|
||||
<SquareImage
|
||||
image={imgs[2]}
|
||||
style={{
|
||||
paddingTop: '48.75%',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
}}
|
||||
insetBorderStyle={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
}}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Grid.Row gutter={gutter}>
|
||||
<Grid.Column gutter={gutter} width={1 / 2} cx={[a.gap_xs]}>
|
||||
<SquareImage
|
||||
image={imgs[0]}
|
||||
style={{
|
||||
// aspect ratio of 1.5 - matches app
|
||||
paddingTop: '66.666%',
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}
|
||||
insetBorderStyle={{
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}
|
||||
/>
|
||||
<SquareImage
|
||||
image={imgs[2]}
|
||||
style={{
|
||||
paddingTop: '66.666%',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
}}
|
||||
insetBorderStyle={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
}}
|
||||
/>
|
||||
</Grid.Column>
|
||||
<Grid.Column gutter={gutter} width={1 / 2} cx={[a.gap_xs]}>
|
||||
<SquareImage
|
||||
image={imgs[1]}
|
||||
style={{
|
||||
paddingTop: '66.666%',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
insetBorderStyle={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
/>
|
||||
<SquareImage
|
||||
image={imgs[3]}
|
||||
style={{
|
||||
paddingTop: '66.666%',
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
}}
|
||||
insetBorderStyle={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
}}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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 * as bsky from '../../types/bsky/index.js'
|
||||
import {getModerationCauseInfo} from '../../util/getModerationCauseInfo.js'
|
||||
import {moderatePost} from '../../util/moderatePost.js'
|
||||
import {sanitizeHandle} from '../../util/sanitizeHandle.js'
|
||||
import {getVerificationState} from '../../util/verificationState.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'
|
||||
import {VerificationCheck} from '../VerificationCheck.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 =
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post,
|
||||
AppBskyFeedPost.isRecord,
|
||||
) && post.text
|
||||
? new RichTextApi({
|
||||
text: post.text,
|
||||
facets: post.facets,
|
||||
})
|
||||
: undefined
|
||||
const postView = viewRecordToPostView(embed)
|
||||
const moderation = moderatePost(postView, moderatorData.moderationOptions)
|
||||
const verification = getVerificationState({profile: embed.author})
|
||||
|
||||
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,
|
||||
{gap: 6},
|
||||
a.pb_xs,
|
||||
a.w_full,
|
||||
a.overflow_hidden,
|
||||
]}>
|
||||
<Avatar
|
||||
size={16}
|
||||
image={avatar}
|
||||
profile={author}
|
||||
moderatorData={moderatorData}
|
||||
/>
|
||||
<Box cx={[a.flex_row, a.align_center, a.gap_xs, a.pt_2xs]}>
|
||||
<Text
|
||||
cx={[
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
a.flex_shrink_0,
|
||||
{maxWidth: '70%'},
|
||||
a.line_clamp_1,
|
||||
]}>
|
||||
{author.displayName || author.handle}
|
||||
</Text>
|
||||
{verification.isVerified && (
|
||||
<Box cx={[{marginTop: -2}]}>
|
||||
<VerificationCheck
|
||||
size={12}
|
||||
verifier={verification.role === 'verifier'}
|
||||
fill={t.palette.primary_500}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Text
|
||||
cx={[
|
||||
a.text_xs,
|
||||
t.atoms.text_contrast_medium,
|
||||
{flexShrink: 10},
|
||||
a.line_clamp_1,
|
||||
]}>
|
||||
{sanitizeHandle(author.handle, '@')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{rt && <RichText value={rt} cx={[a.text_sm]} />}
|
||||
|
||||
{embeds && embeds.length && (
|
||||
<PostEmbed
|
||||
embed={embeds[0]}
|
||||
data={data}
|
||||
moderation={moderation}
|
||||
moderatorData={moderatorData}
|
||||
hideNestedEmbeds
|
||||
cx={[a.pt_sm]}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import {AppBskyRichtextFacet, RichText as RichTextApi} from '@atproto/api'
|
||||
|
||||
import {atoms as a, style as s, theme as t} from '../theme/index.js'
|
||||
import {toShortUrl} from '../util/toShortUrl.js'
|
||||
import {Text} from './Text.js'
|
||||
|
||||
export function RichText({
|
||||
value,
|
||||
disableLinks,
|
||||
cx = [],
|
||||
}: {
|
||||
value: RichTextApi
|
||||
disableLinks?: boolean
|
||||
cx?: Record<string, any>[]
|
||||
}) {
|
||||
const {text, facets} = value
|
||||
const baseStyles = [a.leading_snug, {whiteSpace: 'pre-wrap'}, ...cx]
|
||||
const linkStyles = [
|
||||
{
|
||||
color: t.palette.primary_500,
|
||||
},
|
||||
]
|
||||
const fontSize: number = s(baseStyles).fontSize || a.text_sm.fontSize
|
||||
|
||||
if (!facets?.length) {
|
||||
if (isOnlyEmoji(text)) {
|
||||
return (
|
||||
<Text cx={[...baseStyles, {fontSize: fontSize * 1.85}]}>{text}</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const els = []
|
||||
let key = 0
|
||||
// N.B. must access segments via `richText.segments`, not via destructuring
|
||||
for (const segment of value.segments()) {
|
||||
const {text, link, mention, tag} = segment
|
||||
if (
|
||||
mention &&
|
||||
AppBskyRichtextFacet.validateMention(mention).success &&
|
||||
!disableLinks
|
||||
) {
|
||||
els.push(
|
||||
<span key={key} style={s(linkStyles)}>
|
||||
{text}
|
||||
</span>,
|
||||
)
|
||||
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
|
||||
const url = toShortUrl(text)
|
||||
if (disableLinks) {
|
||||
els.push(url)
|
||||
} else {
|
||||
els.push(
|
||||
<span key={key} style={s(linkStyles)}>
|
||||
{url}
|
||||
</span>,
|
||||
)
|
||||
}
|
||||
} else if (
|
||||
!disableLinks &&
|
||||
tag &&
|
||||
AppBskyRichtextFacet.validateTag(tag).success
|
||||
) {
|
||||
els.push(
|
||||
<span key={key} style={s(linkStyles)}>
|
||||
{text}
|
||||
</span>,
|
||||
)
|
||||
} else {
|
||||
els.push(text)
|
||||
}
|
||||
if (/\n/.test(text)) {
|
||||
els.push(<span key={key} style={{width: '100%'}} />)
|
||||
}
|
||||
key++
|
||||
}
|
||||
|
||||
return <Text cx={baseStyles}>{els}</Text>
|
||||
}
|
||||
|
||||
export function isOnlyEmoji(text: string) {
|
||||
return (
|
||||
text.length <= 15 &&
|
||||
/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text)
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable bsky-internal/avoid-unwrapped-text */
|
||||
import React from 'react'
|
||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||
|
||||
import * as bsky from '../types/bsky/index.js'
|
||||
import {Butterfly} from './Butterfly.js'
|
||||
import {Img} from './Img.js'
|
||||
|
||||
@@ -18,7 +17,10 @@ export function StarterPack(props: {
|
||||
images: Map<string, Buffer>
|
||||
}) {
|
||||
const {starterPack, images} = props
|
||||
const record = AppBskyGraphStarterpack.isRecord(starterPack.record)
|
||||
const record = bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||
starterPack.record,
|
||||
AppBskyGraphStarterpack.isRecord,
|
||||
)
|
||||
? starterPack.record
|
||||
: null
|
||||
const imagesArray = [...images.values()]
|
||||
@@ -106,6 +108,7 @@ export function StarterPack(props: {
|
||||
color: 'white',
|
||||
padding: 60,
|
||||
fontSize: 40,
|
||||
fontWeight: '700',
|
||||
}}>
|
||||
JOIN THE CONVERSATION
|
||||
</div>
|
||||
@@ -134,6 +137,7 @@ export function StarterPack(props: {
|
||||
fontSize: isLongTitle ? 55 : 65,
|
||||
display: 'flex',
|
||||
textAlign: 'center',
|
||||
fontWeight: '700',
|
||||
}}>
|
||||
{record?.name || 'Starter Pack'}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import {atoms as a, style as s, theme as t} from '../theme/index.js'
|
||||
import {FONT_FAMILY_DEF} from '../util/fonts.js'
|
||||
|
||||
const SCALE_MULTIPLIER = 1 - 0.0625
|
||||
|
||||
export function Text({
|
||||
children,
|
||||
cx,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
cx?: Record<string, any>[]
|
||||
}) {
|
||||
const styles = s([
|
||||
a.flex,
|
||||
a.flex_wrap,
|
||||
{
|
||||
columnGap: '1px',
|
||||
},
|
||||
a.font_normal,
|
||||
a.leading_tight,
|
||||
// a.tracking_wide, // 0.25
|
||||
t.atoms.text,
|
||||
{
|
||||
fontFamily: FONT_FAMILY_DEF,
|
||||
fontWeight: 400,
|
||||
},
|
||||
...(cx ?? []),
|
||||
])
|
||||
styles.fontSize = (styles.fontSize || a.text_md.fontSize) * SCALE_MULTIPLIER
|
||||
return <div style={styles}>{children}</div>
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {type IconProps} from './icons/types.js'
|
||||
import {VerifiedCheck} from './icons/VerifiedCheck.js'
|
||||
import {VerifierCheck} from './icons/VerifierCheck.js'
|
||||
|
||||
export function VerificationCheck({
|
||||
verifier,
|
||||
...rest
|
||||
}: IconProps & {
|
||||
verifier?: boolean
|
||||
}) {
|
||||
return verifier ? <VerifierCheck {...rest} /> : <VerifiedCheck {...rest} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function CircleInfo({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-1a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-4a1 1 0 0 1-1-1Zm1-3a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function EyeSlash({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.293 2.293a1 1 0 0 1 1.414 0L7.335 5.92l.03.03 3.22 3.222 4.243 4.242 3.22 3.22.03.03 3.63 3.629a1 1 0 0 1-1.415 1.414l-3.09-3.09c-2.65 1.478-5.625 1.778-8.421.869-3.039-.987-5.779-3.37-7.67-7.027a1 1 0 0 1 0-.918c1.086-2.1 2.452-3.78 3.996-5.019L2.293 3.707a1 1 0 0 1 0-1.414Zm4.24 5.654 2.021 2.021a4 4 0 0 0 5.478 5.478l1.688 1.688c-2.042.982-4.246 1.124-6.32.45-2.34-.76-4.594-2.586-6.265-5.584.97-1.739 2.135-3.083 3.398-4.053Zm3.535 3.535 2.45 2.45a2 2 0 0 1-2.45-2.45Zm.81-5.405c3.573-.49 7.45 1.369 9.987 5.923a14.797 14.797 0 0 1-1.347 2.02 1 1 0 1 0 1.564 1.247 17.078 17.078 0 0 0 1.806-2.808 1 1 0 0 0 0-.918c-2.833-5.479-7.584-8.088-12.281-7.446a1 1 0 0 0 .271 1.982Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function Heart({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47a1 1 0 0 0 .978 0Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function Logomark({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 500 441" width={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M108.382 29.634C165.705 72.668 227.36 159.93 250 206.754c22.64-46.824 84.295-134.084 141.618-177.12C432.98-1.42 500-25.447 500 51.008c0 15.269-8.755 128.269-13.889 146.615-17.848 63.779-82.883 80.047-140.735 70.2 101.122 17.211 126.846 74.218 71.291 131.225-105.511 108.268-151.649-27.165-163.471-61.867-2.167-6.361-3.181-9.338-3.196-6.807-.015-2.531-1.029.446-3.196 6.807-11.822 34.702-57.96 170.135-163.47 61.867-55.556-57.007-29.832-114.014 71.29-131.225-57.852 9.847-122.887-6.421-140.735-70.2C8.755 179.278 0 66.278 0 51.009 0-25.446 67.02-1.419 108.382 29.634Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function Logotype({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 398 108" width={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M53.139 40.276c9.306 3.396 14.247 11.24 14.247 20.022 0 14.87-9.766 23.886-28.494 23.886H.632V.818h36.996c17.809 0 26.426 9.25 26.426 21.544 0 8.196-3.677 14.167-10.915 17.914Zm-16.66-26.462H16.143v21.779h20.336c7.928 0 12.179-4.215 12.179-11.24 0-6.44-4.366-10.539-12.179-10.539ZM16.143 71.07h21.945c8.732 0 13.442-4.098 13.442-11.474 0-7.728-4.48-11.592-13.442-11.592H16.143V71.07ZM88.892 84.184H74.415V.818h14.477v83.366ZM136.892 57.605V23.767h14.477v60.417h-14.017v-8.782c-4.481 6.791-10.686 10.187-18.613 10.187-12.524 0-20.681-7.728-20.681-21.778V23.767h14.476v37.585c0 7.61 3.677 11.474 11.145 11.474 7.009 0 13.213-5.268 13.213-15.22ZM217.264 55.03v3.512h-44.35c1.034 10.42 6.664 15.572 15.396 15.572 6.664 0 11.145-2.927 13.558-8.664h13.902c-3.102 12.294-13.443 20.139-27.575 20.139-8.847 0-15.97-2.927-21.37-8.665-5.4-5.737-8.158-13.347-8.158-22.949 0-9.484 2.643-17.094 8.043-22.949 5.4-5.737 12.409-8.664 21.256-8.664 8.961 0 16.085 3.044 21.37 9.016 5.285 5.971 7.928 13.933 7.928 23.651Zm-29.413-21.194c-7.928 0-13.443 4.684-14.822 14.402h29.758c-1.264-8.781-6.549-14.402-14.936-14.402ZM249.35 85.823c-17.234 0-26.311-6.908-27.115-20.841h14.132c.804 7.493 4.481 10.303 13.213 10.303 7.813 0 11.719-2.459 11.719-7.26 0-4.331-2.757-6.439-11.604-7.961l-6.779-1.17c-12.983-2.226-19.417-8.314-19.417-18.267 0-11.357 8.847-18.265 24.587-18.265 16.89 0 25.622 6.79 26.196 20.49H260.61c-.345-7.376-4.596-9.952-12.524-9.952-6.894 0-10.34 2.342-10.34 7.025 0 4.215 2.987 6.089 9.881 7.376l7.468 1.171c14.362 2.693 20.566 8.08 20.566 18.383 0 12.177-9.651 18.968-26.311 18.968ZM339.201 84.184h-16.545l-17.235-28.101-8.961 9.133v18.968h-14.247V.818h14.247v48.006l24.128-25.057h17.234l-22.405 22.832 23.784 37.585ZM378.008 38.988l4.826-15.221H398L375.136 89.1c-2.413 6.674-5.4 11.475-9.192 14.168-3.791 2.693-9.191 3.981-16.315 3.981-2.413 0-4.481-.117-6.319-.351V95.307h5.515c6.549 0 9.766-4.098 9.766-9.718 0-2.81-.919-6.908-2.758-12.177l-17.234-49.645h15.626l4.825 15.104c3.562 11.358 6.664 22.598 9.422 33.721 2.528-9.6 5.745-20.841 9.536-33.604Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function PlayFilled({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.514 2.143A1 1 0 0 0 5 3v18a1 1 0 0 0 1.514.858l15-9a1 1 0 0 0 0-1.716l-15-9Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function Repost({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.957 2.293a1 1 0 1 0-1.414 1.414L17.836 5H6a3 3 0 0 0-3 3v3a1 1 0 1 0 2 0V8a1 1 0 0 1 1-1h11.836l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.47-2.47a1.75 1.75 0 0 0 0-2.474l-2.47-2.47ZM20 12a1 1 0 0 1 1 1v3a3 3 0 0 1-3 3H6.164l1.293 1.293a1 1 0 1 1-1.414 1.414l-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47a1 1 0 0 1 1.414 1.414L6.164 17H18a1 1 0 0 0 1-1v-3a1 1 0 0 1 1-1Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function StarterPack({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.26 5.227 5.02 6.899c-.734.197-1.17.95-.973 1.685l1.672 6.24c.197.734.951 1.17 1.685.973l6.24-1.672c.734-.197 1.17-.951.973-1.685L12.945 6.2a1.375 1.375 0 0 0-1.685-.973Zm-6.566.459a2.632 2.632 0 0 0-1.86 3.223l1.672 6.24a2.632 2.632 0 0 0 3.223 1.861l6.24-1.672a2.631 2.631 0 0 0 1.861-3.223l-1.672-6.24a2.632 2.632 0 0 0-3.223-1.861l-6.24 1.672Z"
|
||||
/>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.138 18.411a4.606 4.606 0 1 0 0-9.211 4.606 4.606 0 0 0 0 9.211Zm0 1.257a5.862 5.862 0 1 0 0-11.724 5.862 5.862 0 0 0 0 11.724Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function VerifiedCheck({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<circle cx="12" cy="12" r="11.5" fill={fill} />
|
||||
<path
|
||||
fill="#fff"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.659 8.175a1.361 1.361 0 0 1 0 1.925l-6.224 6.223a1.361 1.361 0 0 1-1.925 0L6.4 13.212a1.361 1.361 0 0 1 1.925-1.925l2.149 2.148 5.26-5.26a1.361 1.361 0 0 1 1.925 0Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function VerifierCheck({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.792 1.615a4.154 4.154 0 0 1 6.416 0 4.154 4.154 0 0 0 3.146 1.515 4.154 4.154 0 0 1 4 5.017 4.154 4.154 0 0 0 .777 3.404 4.154 4.154 0 0 1-1.427 6.255 4.153 4.153 0 0 0-2.177 2.73 4.154 4.154 0 0 1-5.781 2.784 4.154 4.154 0 0 0-3.492 0 4.154 4.154 0 0 1-5.78-2.784 4.154 4.154 0 0 0-2.178-2.73A4.154 4.154 0 0 1 .87 11.551a4.154 4.154 0 0 0 .776-3.404A4.154 4.154 0 0 1 5.646 3.13a4.154 4.154 0 0 0 3.146-1.515Z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17.861 8.26a1.438 1.438 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L5.97 13.58a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {IconProps} from './types.js'
|
||||
|
||||
export function Warning({size, fill}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<path
|
||||
fill={fill}
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {IconProps} from '../types.js'
|
||||
|
||||
export function DefaultFeed({size}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 32 32" width={size} height={size}>
|
||||
<rect width="32" height="32" rx="4" fill="#0070FF" />
|
||||
<path
|
||||
d="M13.5 7.25C13.5 6.55859 14.0586 6 14.75 6C20.9648 6 26 11.0352 26 17.25C26 17.9414 25.4414 18.5 24.75 18.5C24.0586 18.5 23.5 17.9414 23.5 17.25C23.5 12.418 19.582 8.5 14.75 8.5C14.0586 8.5 13.5 7.94141 13.5 7.25ZM8.36719 14.6172L12.4336 18.6836L13.543 17.5742C13.5156 17.4727 13.5 17.3633 13.5 17.25C13.5 16.5586 14.0586 16 14.75 16C15.4414 16 16 16.5586 16 17.25C16 17.9414 15.4414 18.5 14.75 18.5C14.6367 18.5 14.5312 18.4844 14.4258 18.457L13.3164 19.5664L17.3828 23.6328C17.9492 24.1992 17.8438 25.1484 17.0977 25.4414C16.1758 25.8008 15.1758 26 14.125 26C9.63672 26 6 22.3633 6 17.875C6 16.8242 6.19922 15.8242 6.5625 14.9023C6.85547 14.1602 7.80469 14.0508 8.37109 14.6172H8.36719ZM14.75 9.75C18.8906 9.75 22.25 13.1094 22.25 17.25C22.25 17.9414 21.6914 18.5 21 18.5C20.3086 18.5 19.75 17.9414 19.75 17.25C19.75 14.4883 17.5117 12.25 14.75 12.25C14.0586 12.25 13.5 11.6914 13.5 11C13.5 10.3086 14.0586 9.75 14.75 9.75Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {IconProps} from '../types.js'
|
||||
|
||||
export function DefaultList({size}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 32 32" width={size} height={size}>
|
||||
<path
|
||||
d="M28 0H4C1.79086 0 0 1.79086 0 4V28C0 30.2091 1.79086 32 4 32H28C30.2091 32 32 30.2091 32 28V4C32 1.79086 30.2091 0 28 0Z"
|
||||
fill="#0070FF"
|
||||
/>
|
||||
<path
|
||||
d="M22.1529 22.3542C23.4522 22.4603 24.7593 22.293 25.9899 21.8629C26.0369 21.2838 25.919 20.7032 25.6497 20.1884C25.3805 19.6735 24.9711 19.2454 24.4687 18.9535C23.9663 18.6617 23.3916 18.518 22.8109 18.5392C22.2303 18.5603 21.6676 18.7454 21.1878 19.0731M22.1529 22.3542C22.1489 21.1917 21.8142 20.0534 21.1878 19.0741ZM10.8111 19.0741C10.3313 18.7468 9.7687 18.5619 9.18826 18.5409C8.60781 18.5199 8.03327 18.6636 7.53107 18.9554C7.02888 19.2472 6.61953 19.6752 6.35036 20.1899C6.08119 20.7046 5.96319 21.285 6.01001 21.8639C7.23969 22.2964 8.5461 22.4632 9.84497 22.3531M10.8111 19.0741C10.1851 20.0535 9.84865 21.1908 9.84497 22.3531ZM19.0759 10.077C19.0759 10.8931 18.7518 11.6757 18.1747 12.2527C17.5977 12.8298 16.815 13.154 15.9989 13.154C15.1829 13.154 14.4002 12.8298 13.8232 12.2527C13.2461 11.6757 12.922 10.8931 12.922 10.077C12.922 9.26092 13.2461 8.47828 13.8232 7.90123C14.4002 7.32418 15.1829 7 15.9989 7C16.815 7 17.5977 7.32418 18.1747 7.90123C18.7518 8.47828 19.0759 9.26092 19.0759 10.077ZM25.2299 13.154C25.2299 13.457 25.1702 13.7571 25.0542 14.0371C24.9383 14.3171 24.7683 14.5715 24.554 14.7858C24.3397 15.0001 24.0853 15.1701 23.8053 15.2861C23.5253 15.402 23.2252 15.4617 22.9222 15.4617C22.6191 15.4617 22.319 15.402 22.039 15.2861C21.759 15.1701 21.5046 15.0001 21.2903 14.7858C21.0761 14.5715 20.9061 14.3171 20.7901 14.0371C20.6741 13.7571 20.6144 13.457 20.6144 13.154C20.6144 12.5419 20.8576 11.9549 21.2903 11.5222C21.7231 11.0894 22.3101 10.8462 22.9222 10.8462C23.5342 10.8462 24.1212 11.0894 24.554 11.5222C24.9868 11.9549 25.2299 12.5419 25.2299 13.154ZM11.3835 13.154C11.3835 13.457 11.3238 13.7571 11.2078 14.0371C11.0918 14.3171 10.9218 14.5715 10.7075 14.7858C10.4932 15.0001 10.2388 15.1701 9.95886 15.2861C9.67887 15.402 9.37878 15.4617 9.07572 15.4617C8.77266 15.4617 8.47257 15.402 8.19259 15.2861C7.9126 15.1701 7.6582 15.0001 7.4439 14.7858C7.22961 14.5715 7.05962 14.3171 6.94365 14.0371C6.82767 13.7571 6.76798 13.457 6.76798 13.154C6.76798 12.5419 7.01112 11.9549 7.4439 11.5222C7.87669 11.0894 8.46367 10.8462 9.07572 10.8462C9.68777 10.8462 10.2748 11.0894 10.7075 11.5222C11.1403 11.9549 11.3835 12.5419 11.3835 13.154Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M22 22C22 25.3137 19.3137 25.5 16 25.5C12.6863 25.5 10 25.3137 10 22C10 18.6863 12.6863 16 16 16C19.3137 16 22 18.6863 22 22Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import {IconProps} from '../types.js'
|
||||
|
||||
export function DefaultUser({size}: IconProps) {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
|
||||
<circle cx="12" cy="12" r="12" fill="#0070ff" />
|
||||
<circle cx="12" cy="9.5" r="3.5" fill="#fff" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="#fff"
|
||||
d="M 12.058 22.784 C 9.422 22.784 7.007 21.836 5.137 20.262 C 5.667 17.988 8.534 16.25 11.99 16.25 C 15.494 16.25 18.391 18.036 18.864 20.357 C 17.01 21.874 14.64 22.784 12.058 22.784 Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type IconProps = {size: number; fill?: string}
|
||||
@@ -1,12 +1,7 @@
|
||||
import {readdirSync, readFileSync} from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
|
||||
import {AtpAgent} from '@atproto/api'
|
||||
|
||||
import {Config} from './config.js'
|
||||
|
||||
const __DIRNAME = path.dirname(fileURLToPath(import.meta.url))
|
||||
import {getFontDefinitions} from './util/fonts.js'
|
||||
|
||||
export type AppContextOptions = {
|
||||
cfg: Config
|
||||
@@ -28,14 +23,8 @@ export class AppContext {
|
||||
|
||||
static async fromConfig(cfg: Config, overrides?: Partial<AppContextOptions>) {
|
||||
const appviewAgent = new AtpAgent({service: cfg.service.appviewUrl})
|
||||
const fontDirectory = path.join(__DIRNAME, 'assets', 'fonts')
|
||||
const fontFiles = readdirSync(fontDirectory)
|
||||
const fonts = fontFiles.map(file => {
|
||||
return {
|
||||
name: path.basename(file, path.extname(file)),
|
||||
data: readFileSync(path.join(fontDirectory, file)),
|
||||
}
|
||||
})
|
||||
const fonts = getFontDefinitions()
|
||||
|
||||
return new AppContext({
|
||||
cfg,
|
||||
appviewAgent,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import MIMEType from 'whatwg-mimetype'
|
||||
|
||||
export async function getImage(url: string) {
|
||||
const response = await fetch(url)
|
||||
const mimeType = new MIMEType(response.headers.get('content-type') ?? '')
|
||||
const arrayBuf = await response.arrayBuffer() // must drain body even if it will be discarded
|
||||
if (response.status !== 200) return null
|
||||
// TODO suss
|
||||
const overwriteOctectStream =
|
||||
url.endsWith('.jpg') && mimeType.essence === 'application/octet-stream'
|
||||
return {
|
||||
mime: overwriteOctectStream
|
||||
? 'image/jpeg'
|
||||
: (mimeType.essence as string | undefined),
|
||||
image: Buffer.from(arrayBuf),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import {
|
||||
AppBskyLabelerDefs,
|
||||
AtpAgent,
|
||||
BSKY_LABELER_DID,
|
||||
DEFAULT_LABEL_SETTINGS,
|
||||
interpretLabelValueDefinitions,
|
||||
moderatePost,
|
||||
} from '@atproto/api'
|
||||
|
||||
export type ModeratorData = Awaited<ReturnType<typeof getModeratorData>>
|
||||
|
||||
export const DEFAULT_LABELS: typeof DEFAULT_LABEL_SETTINGS = Object.fromEntries(
|
||||
Object.entries(DEFAULT_LABEL_SETTINGS).map(([key, _pref]) => [key, 'hide']),
|
||||
)
|
||||
|
||||
export async function getModeratorData(agent: AtpAgent) {
|
||||
const {data} = await agent.app.bsky.labeler.getServices({
|
||||
dids: [BSKY_LABELER_DID],
|
||||
detailed: true,
|
||||
})
|
||||
if (!data || !data.views[0]) {
|
||||
throw new Error(`Could not fetch label definitions`)
|
||||
}
|
||||
const labeler = data.views[0] as AppBskyLabelerDefs.LabelerViewDetailed
|
||||
const definitions = interpretLabelValueDefinitions(labeler)
|
||||
const moderationOptions: Parameters<typeof moderatePost>[1] = {
|
||||
userDid: undefined,
|
||||
prefs: {
|
||||
adultContentEnabled: false,
|
||||
labels: DEFAULT_LABELS,
|
||||
mutedWords: [],
|
||||
hiddenPosts: [],
|
||||
labelers: [
|
||||
{
|
||||
did: BSKY_LABELER_DID,
|
||||
labels: DEFAULT_LABELS,
|
||||
},
|
||||
],
|
||||
},
|
||||
labelDefs: {
|
||||
[BSKY_LABELER_DID]: definitions,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
labeler,
|
||||
moderationOptions,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {AtpAgent, AtUri} from '@atproto/api'
|
||||
|
||||
export async function getPost({uri, agent}: {uri: AtUri; agent: AtpAgent}) {
|
||||
if (!uri.hostname.startsWith('did:')) {
|
||||
const res = await agent.resolveHandle({
|
||||
handle: uri.hostname,
|
||||
})
|
||||
uri.hostname = res.data.did
|
||||
}
|
||||
const {data} = await agent.getPosts({
|
||||
uris: [uri.toString()],
|
||||
})
|
||||
return data.posts.at(0)
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import {AppBskyFeedDefs} from '@atproto/api'
|
||||
import {Vibrant} from 'node-vibrant/node'
|
||||
|
||||
import {httpLogger} from '../logger.js'
|
||||
import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js'
|
||||
import {Embed, parseEmbed} from '../util/parseEmbed.js'
|
||||
import {parseEmbedPlayerFromUrl} from '../util/parseEmbedPlayerFromUrl.js'
|
||||
import {getImage} from './getImage.js'
|
||||
|
||||
export type Metadata = {
|
||||
colors?: {
|
||||
vibrant: string
|
||||
darkVibrant: string
|
||||
lightVibrant: string
|
||||
muted: string
|
||||
darkMuted: string
|
||||
lightMuted: string
|
||||
}
|
||||
aspectRatio: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
}
|
||||
|
||||
export type Image = Metadata & {
|
||||
mime: 'image/jpeg' | 'image/png' | string | undefined
|
||||
image: Buffer
|
||||
}
|
||||
|
||||
export type PostData = {
|
||||
images: Map<string, Image>
|
||||
}
|
||||
|
||||
function normalizeAspectRatio(aspectRatio?: {
|
||||
width: number
|
||||
height: number
|
||||
}): Metadata['aspectRatio'] {
|
||||
if (!aspectRatio)
|
||||
return {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
}
|
||||
return aspectRatio
|
||||
}
|
||||
|
||||
export function getEmbedData(embed: Embed, images: Map<string, Metadata>) {
|
||||
switch (embed.type) {
|
||||
case 'images': {
|
||||
for (const image of embed.view.images) {
|
||||
images.set(image.fullsize, {
|
||||
aspectRatio: normalizeAspectRatio(image.aspectRatio),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'link': {
|
||||
if (embed.view.external.thumb) {
|
||||
images.set(embed.view.external.thumb, {
|
||||
aspectRatio: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
})
|
||||
}
|
||||
const player = parseEmbedPlayerFromUrl(embed.view.external.uri)
|
||||
if (player) {
|
||||
if (['giphy_gif', 'tenor_gif'].includes(player.type)) {
|
||||
images.set(player.playerUri, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'video': {
|
||||
if (embed.view.thumbnail) {
|
||||
images.set(embed.view.thumbnail, {
|
||||
aspectRatio: normalizeAspectRatio(embed.view.aspectRatio),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'feed': {
|
||||
if (embed.view.avatar) {
|
||||
images.set(embed.view.avatar, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'list': {
|
||||
if (embed.view.avatar) {
|
||||
images.set(embed.view.avatar, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'starter_pack': {
|
||||
const uri = getStarterPackImageUri(embed.view)
|
||||
images.set(uri, {
|
||||
aspectRatio: {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'post': {
|
||||
if (embed.view.author.avatar) {
|
||||
images.set(embed.view.author.avatar, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (embed.view.embeds) {
|
||||
for (const _e of embed.view.embeds) {
|
||||
getEmbedData(parseEmbed(_e), images)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'post_with_media': {
|
||||
getEmbedData(embed.view, images)
|
||||
getEmbedData(embed.media, images)
|
||||
break
|
||||
}
|
||||
case 'post_blocked':
|
||||
case 'post_detached':
|
||||
case 'post_not_found':
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export async function getImageColors(image: Buffer) {
|
||||
const palette = await Vibrant.from(image).getPalette()
|
||||
return {
|
||||
vibrant: palette.Vibrant?.hex,
|
||||
darkVibrant: palette.DarkVibrant?.hex,
|
||||
lightVibrant: palette.LightVibrant?.hex,
|
||||
muted: palette.Muted?.hex,
|
||||
darkMuted: palette.DarkMuted?.hex,
|
||||
lightMuted: palette.LightMuted?.hex,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPostData(
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
): Promise<PostData> {
|
||||
const images: Map<string, Metadata> = new Map()
|
||||
|
||||
if (post.author.avatar) {
|
||||
images.set(post.author.avatar, {
|
||||
aspectRatio: {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (post.embed) {
|
||||
getEmbedData(parseEmbed(post.embed), images)
|
||||
}
|
||||
|
||||
const deduped = Array.from(images.entries())
|
||||
const resolved = await Promise.all(
|
||||
deduped.map(async ([url, meta]) => {
|
||||
try {
|
||||
const imageData = await getImage(url)
|
||||
const palette = await getImageColors(imageData.image)
|
||||
return [
|
||||
url,
|
||||
{
|
||||
colors: palette,
|
||||
...meta,
|
||||
...imageData,
|
||||
},
|
||||
] as const
|
||||
} catch (err) {
|
||||
httpLogger.warn({err, uri: url}, 'could not fetch image')
|
||||
return [
|
||||
url,
|
||||
{
|
||||
...meta,
|
||||
image: null,
|
||||
},
|
||||
] as const
|
||||
}
|
||||
}),
|
||||
)
|
||||
const extracted = resolved.filter(([, i]) => i.image !== null) as [
|
||||
string,
|
||||
Image,
|
||||
][]
|
||||
|
||||
return {
|
||||
images: new Map(extracted),
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import {Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {default as health} from './health.js'
|
||||
import {default as post} from './post.js'
|
||||
import {default as starterPack} from './starter-pack.js'
|
||||
|
||||
export * from './util.js'
|
||||
@@ -9,5 +10,6 @@ export * from './util.js'
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
app = health(ctx, app) // GET /_health
|
||||
app = starterPack(ctx, app) // GET /start/:actor/:rkey
|
||||
app = post(ctx, app) // POST /profile/:handle/post/:rkey
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import {AppBskyFeedPost, AtUri} from '@atproto/api'
|
||||
import resvg from '@resvg/resvg-js'
|
||||
import {Express} from 'express'
|
||||
import satori from 'satori'
|
||||
|
||||
import {Post} from '../components/Post/index.js'
|
||||
import {AppContext} from '../context.js'
|
||||
import {getModeratorData} from '../data/getModeratorData.js'
|
||||
import {getPost} from '../data/getPost.js'
|
||||
import {getPostData} from '../data/getPostData.js'
|
||||
import {httpLogger} from '../logger.js'
|
||||
import {loadEmojiAsSvg} from '../util.js'
|
||||
import {
|
||||
getRenderOptions,
|
||||
parseDisplayOptionsFromQuery,
|
||||
} from '../util/postDisplayOptions.js'
|
||||
import {handler, originVerifyMiddleware} from './util.js'
|
||||
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
return app.get(
|
||||
'/profile/:actor/post/:rkey',
|
||||
originVerifyMiddleware(ctx),
|
||||
handler(async (req, res) => {
|
||||
let {actor, rkey} = req.params
|
||||
let uri = AtUri.make(actor, 'app.bsky.feed.post', rkey)
|
||||
|
||||
try {
|
||||
const post = await getPost({
|
||||
uri,
|
||||
agent: ctx.appviewAgent,
|
||||
})
|
||||
|
||||
if (!AppBskyFeedPost.isRecord(post.record)) {
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
const notPublic = post.author.labels.some(
|
||||
l => l.val === `!no-unauthenticated`,
|
||||
)
|
||||
if (notPublic) {
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
|
||||
const [postData, moderatorData] = await Promise.all([
|
||||
/*
|
||||
* Fetch any remote post data, like images and their metadata.
|
||||
*/
|
||||
getPostData(post),
|
||||
/*
|
||||
* Fetches labeler definitions and builds `moderationOpts`
|
||||
*/
|
||||
getModeratorData(ctx.appviewAgent),
|
||||
])
|
||||
const displayOptions = parseDisplayOptionsFromQuery(req.query)
|
||||
const {width} = getRenderOptions(displayOptions)
|
||||
|
||||
const svg = await satori(
|
||||
<Post
|
||||
post={post}
|
||||
data={postData}
|
||||
moderatorData={moderatorData}
|
||||
displayOptions={displayOptions}
|
||||
/>,
|
||||
{
|
||||
fonts: ctx.fonts,
|
||||
width,
|
||||
loadAdditionalAsset: async (code, text) => {
|
||||
if (code === 'emoji') {
|
||||
return await loadEmojiAsSvg(text)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
const output = await resvg.renderAsync(svg, {
|
||||
fitTo: {
|
||||
mode: 'width',
|
||||
value: width * 2,
|
||||
},
|
||||
// logLevel: 'trace',
|
||||
})
|
||||
|
||||
res.statusCode = 200
|
||||
res.setHeader('content-type', 'image/png')
|
||||
res.setHeader('cdn-tag', [...postData.images.keys()].join(','))
|
||||
|
||||
return res.end(output.asPng())
|
||||
} catch (err) {
|
||||
httpLogger.warn(
|
||||
{err, uri: uri.toString()},
|
||||
`Failed to render post ${uri}`,
|
||||
)
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import assert from 'node:assert'
|
||||
|
||||
import React from 'react'
|
||||
import {AppBskyGraphDefs, AtUri} from '@atproto/api'
|
||||
import {AppBskyActorDefs, AppBskyGraphDefs, AtUri} from '@atproto/api'
|
||||
import resvg from '@resvg/resvg-js'
|
||||
import {Express} from 'express'
|
||||
import satori from 'satori'
|
||||
@@ -37,7 +36,13 @@ export default function (ctx: AppContext, app: Express) {
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
const imageEntries = await Promise.all(
|
||||
[starterPack.creator]
|
||||
(
|
||||
[] as (
|
||||
| AppBskyActorDefs.ProfileViewBasic
|
||||
| AppBskyActorDefs.ProfileView
|
||||
)[]
|
||||
)
|
||||
.concat([starterPack.creator])
|
||||
.concat(starterPack.listItemsSample.map(li => li.subject))
|
||||
// has avatar
|
||||
.filter(p => p.avatar)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {atoms as baseAtoms} from '@bsky.app/alf'
|
||||
|
||||
export const atoms = {
|
||||
...baseAtoms,
|
||||
inline_block: {
|
||||
display: 'inline-block',
|
||||
},
|
||||
text_overflow_ellipsis: {
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
text_overflow_clip: {
|
||||
textOverflow: 'clip',
|
||||
},
|
||||
line_clamp_1: {
|
||||
display: 'block',
|
||||
lineClamp: 1,
|
||||
},
|
||||
line_clamp_2: {
|
||||
display: 'block',
|
||||
lineClamp: 2,
|
||||
},
|
||||
line_clamp_3: {
|
||||
display: 'block',
|
||||
lineClamp: 3,
|
||||
},
|
||||
line_clamp_4: {
|
||||
display: 'block',
|
||||
lineClamp: 4,
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,84 @@
|
||||
export const BLUE_HUE = 211
|
||||
export const RED_HUE = 346
|
||||
export const GREEN_HUE = 152
|
||||
|
||||
/**
|
||||
* Smooth progression of lightness "stops" for generating HSL colors.
|
||||
*/
|
||||
export const COLOR_STOPS = [
|
||||
0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1,
|
||||
]
|
||||
|
||||
export function generateScale(start: number, end: number) {
|
||||
const range = end - start
|
||||
return COLOR_STOPS.map(stop => {
|
||||
return start + range * stop
|
||||
})
|
||||
}
|
||||
|
||||
export const defaultScale = generateScale(6, 100)
|
||||
// dim shifted 6% lighter
|
||||
export const dimScale = generateScale(12, 100)
|
||||
|
||||
export const colors = {
|
||||
white: '#FFFFFF',
|
||||
black: '#000000',
|
||||
|
||||
contrast_0: `hsl(${BLUE_HUE}, 20%, ${defaultScale[14]}%)`,
|
||||
contrast_25: `hsl(${BLUE_HUE}, 20%, ${defaultScale[13]}%)`,
|
||||
contrast_50: `hsl(${BLUE_HUE}, 20%, ${defaultScale[12]}%)`,
|
||||
contrast_100: `hsl(${BLUE_HUE}, 20%, ${defaultScale[11]}%)`,
|
||||
contrast_200: `hsl(${BLUE_HUE}, 20%, ${defaultScale[10]}%)`,
|
||||
contrast_300: `hsl(${BLUE_HUE}, 20%, ${defaultScale[9]}%)`,
|
||||
contrast_400: `hsl(${BLUE_HUE}, 20%, ${defaultScale[8]}%)`,
|
||||
contrast_500: `hsl(${BLUE_HUE}, 20%, ${defaultScale[7]}%)`,
|
||||
contrast_600: `hsl(${BLUE_HUE}, 24%, ${defaultScale[6]}%)`,
|
||||
contrast_700: `hsl(${BLUE_HUE}, 24%, ${defaultScale[5]}%)`,
|
||||
contrast_800: `hsl(${BLUE_HUE}, 28%, ${defaultScale[4]}%)`,
|
||||
contrast_900: `hsl(${BLUE_HUE}, 28%, ${defaultScale[3]}%)`,
|
||||
contrast_950: `hsl(${BLUE_HUE}, 28%, ${defaultScale[2]}%)`,
|
||||
contrast_975: `hsl(${BLUE_HUE}, 28%, ${defaultScale[1]}%)`,
|
||||
contrast_1000: `hsl(${BLUE_HUE}, 28%, ${defaultScale[0]}%)`,
|
||||
|
||||
primary_25: `hsl(${BLUE_HUE}, 99%, 97%)`,
|
||||
primary_50: `hsl(${BLUE_HUE}, 99%, 95%)`,
|
||||
primary_100: `hsl(${BLUE_HUE}, 99%, 90%)`,
|
||||
primary_200: `hsl(${BLUE_HUE}, 99%, 80%)`,
|
||||
primary_300: `hsl(${BLUE_HUE}, 99%, 70%)`,
|
||||
primary_400: `hsl(${BLUE_HUE}, 99%, 60%)`,
|
||||
primary_500: `hsl(${BLUE_HUE}, 99%, 53%)`,
|
||||
primary_600: `hsl(${BLUE_HUE}, 99%, 42%)`,
|
||||
primary_700: `hsl(${BLUE_HUE}, 99%, 34%)`,
|
||||
primary_800: `hsl(${BLUE_HUE}, 99%, 26%)`,
|
||||
primary_900: `hsl(${BLUE_HUE}, 99%, 18%)`,
|
||||
primary_950: `hsl(${BLUE_HUE}, 99%, 10%)`,
|
||||
primary_975: `hsl(${BLUE_HUE}, 99%, 7%)`,
|
||||
|
||||
green_25: `hsl(${GREEN_HUE}, 82%, 97%)`,
|
||||
green_50: `hsl(${GREEN_HUE}, 82%, 95%)`,
|
||||
green_100: `hsl(${GREEN_HUE}, 82%, 90%)`,
|
||||
green_200: `hsl(${GREEN_HUE}, 82%, 80%)`,
|
||||
green_300: `hsl(${GREEN_HUE}, 82%, 70%)`,
|
||||
green_400: `hsl(${GREEN_HUE}, 82%, 60%)`,
|
||||
green_500: `hsl(${GREEN_HUE}, 82%, 50%)`,
|
||||
green_600: `hsl(${GREEN_HUE}, 82%, 42%)`,
|
||||
green_700: `hsl(${GREEN_HUE}, 82%, 34%)`,
|
||||
green_800: `hsl(${GREEN_HUE}, 82%, 26%)`,
|
||||
green_900: `hsl(${GREEN_HUE}, 82%, 18%)`,
|
||||
green_950: `hsl(${GREEN_HUE}, 82%, 10%)`,
|
||||
green_975: `hsl(${GREEN_HUE}, 82%, 7%)`,
|
||||
|
||||
red_25: `hsl(${RED_HUE}, 91%, 97%)`,
|
||||
red_50: `hsl(${RED_HUE}, 91%, 95%)`,
|
||||
red_100: `hsl(${RED_HUE}, 91%, 90%)`,
|
||||
red_200: `hsl(${RED_HUE}, 91%, 80%)`,
|
||||
red_300: `hsl(${RED_HUE}, 91%, 70%)`,
|
||||
red_400: `hsl(${RED_HUE}, 91%, 60%)`,
|
||||
red_500: `hsl(${RED_HUE}, 91%, 50%)`,
|
||||
red_600: `hsl(${RED_HUE}, 91%, 42%)`,
|
||||
red_700: `hsl(${RED_HUE}, 91%, 34%)`,
|
||||
red_800: `hsl(${RED_HUE}, 91%, 26%)`,
|
||||
red_900: `hsl(${RED_HUE}, 91%, 18%)`,
|
||||
red_950: `hsl(${RED_HUE}, 91%, 10%)`,
|
||||
red_975: `hsl(${RED_HUE}, 91%, 7%)`,
|
||||
} as const
|
||||
@@ -0,0 +1,109 @@
|
||||
import {colors} from './colors.js'
|
||||
import * as tokens from './tokens.js'
|
||||
|
||||
export {atoms} from './atoms.js'
|
||||
export * as tokens from './tokens.js'
|
||||
|
||||
const fontFamily = 'Inter'
|
||||
|
||||
export const theme = {
|
||||
name: 'light',
|
||||
palette: colors,
|
||||
atoms: {
|
||||
text: {
|
||||
color: colors.black,
|
||||
fontFamily,
|
||||
},
|
||||
text_contrast_low: {
|
||||
color: colors.contrast_500,
|
||||
fontFamily,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: colors.contrast_700,
|
||||
fontFamily,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: colors.contrast_900,
|
||||
fontFamily,
|
||||
},
|
||||
text_inverted: {
|
||||
color: colors.white,
|
||||
fontFamily,
|
||||
},
|
||||
bg: {
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
bg_contrast_25: {
|
||||
backgroundColor: colors.contrast_25,
|
||||
},
|
||||
bg_contrast_50: {
|
||||
backgroundColor: colors.contrast_50,
|
||||
},
|
||||
bg_contrast_100: {
|
||||
backgroundColor: colors.contrast_100,
|
||||
},
|
||||
bg_contrast_200: {
|
||||
backgroundColor: colors.contrast_200,
|
||||
},
|
||||
bg_contrast_300: {
|
||||
backgroundColor: colors.contrast_300,
|
||||
},
|
||||
bg_contrast_400: {
|
||||
backgroundColor: colors.contrast_400,
|
||||
},
|
||||
bg_contrast_500: {
|
||||
backgroundColor: colors.contrast_500,
|
||||
},
|
||||
bg_contrast_600: {
|
||||
backgroundColor: colors.contrast_600,
|
||||
},
|
||||
bg_contrast_700: {
|
||||
backgroundColor: colors.contrast_700,
|
||||
},
|
||||
bg_contrast_800: {
|
||||
backgroundColor: colors.contrast_800,
|
||||
},
|
||||
bg_contrast_900: {
|
||||
backgroundColor: colors.contrast_900,
|
||||
},
|
||||
bg_contrast_950: {
|
||||
backgroundColor: colors.contrast_950,
|
||||
},
|
||||
bg_contrast_975: {
|
||||
backgroundColor: colors.contrast_975,
|
||||
},
|
||||
bg_primary_500: {
|
||||
backgroundColor: colors.primary_500,
|
||||
},
|
||||
border_contrast_low: {
|
||||
borderColor: colors.contrast_100,
|
||||
},
|
||||
border_contrast_medium: {
|
||||
borderColor: colors.contrast_200,
|
||||
},
|
||||
border_contrast_high: {
|
||||
borderColor: colors.contrast_300,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export type StyleObject = Record<string, any>
|
||||
|
||||
export type StyleProp = {
|
||||
style?: StyleObject
|
||||
}
|
||||
|
||||
export function style(
|
||||
styleObjects: (StyleObject | boolean | null | undefined)[],
|
||||
) {
|
||||
return Object.assign({}, ...styleObjects.filter(Boolean).flat())
|
||||
}
|
||||
|
||||
export function gradient(color: keyof typeof tokens.gradients) {
|
||||
const {values} = tokens.gradients[color]
|
||||
return values
|
||||
.map(([pos, color]) => {
|
||||
return `${color} ${pos * 100}%`
|
||||
})
|
||||
.join(', ')
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
export const space = {
|
||||
_2xs: 2,
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
xl: 20,
|
||||
_2xl: 24,
|
||||
_3xl: 28,
|
||||
_4xl: 32,
|
||||
_5xl: 40,
|
||||
} as const
|
||||
|
||||
export const fontSize = {
|
||||
_2xs: 10,
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 18,
|
||||
xl: 20,
|
||||
_2xl: 22,
|
||||
_3xl: 26,
|
||||
_4xl: 32,
|
||||
_5xl: 40,
|
||||
} as const
|
||||
|
||||
export const lineHeight = {
|
||||
none: 1,
|
||||
normal: 1.5,
|
||||
relaxed: 1.625,
|
||||
} as const
|
||||
|
||||
export const borderRadius = {
|
||||
_2xs: 2,
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 20,
|
||||
full: 999,
|
||||
} as const
|
||||
|
||||
export const fontWeight = {
|
||||
normal: '400',
|
||||
semibold: '500',
|
||||
bold: '600',
|
||||
heavy: '700',
|
||||
} as const
|
||||
|
||||
export const gradients = {
|
||||
sky: {
|
||||
values: [
|
||||
[0, '#0A7AFF'],
|
||||
[1, '#59B9FF'],
|
||||
],
|
||||
hover_value: '#0A7AFF',
|
||||
},
|
||||
midnight: {
|
||||
values: [
|
||||
[0, '#022C5E'],
|
||||
[1, '#4079BC'],
|
||||
],
|
||||
hover_value: '#022C5E',
|
||||
},
|
||||
sunrise: {
|
||||
values: [
|
||||
[0, '#4E90AE'],
|
||||
[0.4, '#AEA3AB'],
|
||||
[0.8, '#E6A98F'],
|
||||
[1, '#F3A84C'],
|
||||
],
|
||||
hover_value: '#AEA3AB',
|
||||
},
|
||||
sunset: {
|
||||
values: [
|
||||
[0, '#6772AF'],
|
||||
[0.6, '#B88BB6'],
|
||||
[1, '#FFA6AC'],
|
||||
],
|
||||
hover_value: '#B88BB6',
|
||||
},
|
||||
summer: {
|
||||
values: [
|
||||
[0, '#FF6A56'],
|
||||
[0.3, '#FF9156'],
|
||||
[1, '#FFDD87'],
|
||||
],
|
||||
hover_value: '#FF9156',
|
||||
},
|
||||
nordic: {
|
||||
values: [
|
||||
[0, '#083367'],
|
||||
[1, '#9EE8C1'],
|
||||
],
|
||||
hover_value: '#3A7085',
|
||||
},
|
||||
bonfire: {
|
||||
values: [
|
||||
[0, '#203E4E'],
|
||||
[0.4, '#755B62'],
|
||||
[0.8, '#CD7765'],
|
||||
[1, '#EF956E'],
|
||||
],
|
||||
hover_value: '#755B62',
|
||||
},
|
||||
neutral: {
|
||||
values: [
|
||||
[0, '#E0E5EB'],
|
||||
[1, '#EFF2F5'],
|
||||
],
|
||||
hover_value: '#755B62',
|
||||
},
|
||||
} as const
|
||||
@@ -0,0 +1,49 @@
|
||||
import {ValidationResult} from '@atproto/lexicon'
|
||||
|
||||
export * as profile from './profile.js'
|
||||
|
||||
/**
|
||||
* Fast type checking without full schema validation, for use with data we
|
||||
* trust, or for non-critical path use cases. Why? Our SDK's `is*` identity
|
||||
* utils do not assert the type of the entire object, only the `$type` string.
|
||||
*
|
||||
* For full validation of the object schema, use the `validate` export from
|
||||
* this file.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import * as bsky from '#/types/bsky'
|
||||
*
|
||||
* if (bsky.dangerousIsType<AppBskyFeedPost.Record>(item, AppBskyFeedPost.isRecord)) {
|
||||
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function dangerousIsType<R extends {$type?: string}>(
|
||||
record: unknown,
|
||||
identity: <V>(v: V) => v is V & {$type: NonNullable<R['$type']>},
|
||||
): record is R {
|
||||
return identity(record)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully validates the object schema, which has a performance cost.
|
||||
*
|
||||
* For faster checks with data we trust, like that from our app view, use the
|
||||
* `dangerousIsType` export from this same file.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import * as bsky from '#/types/bsky'
|
||||
*
|
||||
* if (bsky.validate(item, AppBskyFeedPost.validateRecord)) {
|
||||
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function validate<R extends {$type?: string}>(
|
||||
record: unknown,
|
||||
validator: (v: unknown) => ValidationResult<R>,
|
||||
): record is R {
|
||||
return validator(record).success
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import {type AppBskyActorDefs, type ChatBskyActorDefs} from '@atproto/api'
|
||||
|
||||
/**
|
||||
* Matches any profile view exported by our SDK
|
||||
*/
|
||||
export type AnyProfileView =
|
||||
| AppBskyActorDefs.ProfileViewBasic
|
||||
| AppBskyActorDefs.ProfileView
|
||||
| AppBskyActorDefs.ProfileViewDetailed
|
||||
| ChatBskyActorDefs.ProfileViewBasic
|
||||
@@ -0,0 +1,74 @@
|
||||
import {readdirSync, readFileSync} from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
|
||||
const __DIRNAME = path.join(process.cwd(), 'src')
|
||||
|
||||
export const ADDITIONAL_FONTS = [
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-jp@5.0/japanese-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-jp@5.0/japanese-400-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-tc@5.0/chinese-traditional-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-tc@5.0/chinese-traditional-400-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-sc@5.0/chinese-simplified-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-sc@5.0/chinese-simplified-400-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hk@5.0/chinese-hongkong-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hk@5.0/chinese-hongkong-400-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-kr@5.0/korean-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-kr@5.0/korean-400-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-thai@5.0/thai-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-thai@5.0/thai-400-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-arabic@5.0/arabic-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-arabic@5.0/arabic-400-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hebrew@5.0/hebrew-700-normal.ttf',
|
||||
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hebrew@5.0/hebrew-400-normal.ttf',
|
||||
]
|
||||
|
||||
/**
|
||||
* CSS `fontFamily` defs must use the PostScript names of the fonts. When
|
||||
* adding additional fonts above, you'll need to figure out the PostScript name
|
||||
* and add it to the map below.
|
||||
*/
|
||||
export const ADDITIONAL_FONTS_POSTSCRIPT_NAMES: Record<string, string> = {
|
||||
arabic: 'Noto Sans Arabic',
|
||||
hebrew: 'Noto Sans Hebrew',
|
||||
japanese: 'Noto Sans JP Thin',
|
||||
korean: 'Noto Sans KR',
|
||||
thai: 'Noto Sans Thai',
|
||||
'chinese-hongkong': 'Noto Sans HK',
|
||||
'chinese-simplified': 'Noto Sans SC',
|
||||
'chinese-traditional': 'Noto Sans TC',
|
||||
}
|
||||
|
||||
/**
|
||||
* Precomputed, for use in the runtime CSS defs
|
||||
*/
|
||||
export const FONT_FAMILY_DEF = `Inter, ${Object.values(
|
||||
ADDITIONAL_FONTS_POSTSCRIPT_NAMES,
|
||||
).join(', ')}, sans-serif`
|
||||
|
||||
/**
|
||||
* Get all TTF files from our fonts dir
|
||||
*/
|
||||
export function getFontFiles() {
|
||||
const fontDirectory = path.join(__DIRNAME, 'assets', 'fonts')
|
||||
return readdirSync(fontDirectory)
|
||||
.filter(file => file.endsWith('.ttf'))
|
||||
.map(file => path.join(fontDirectory, file))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all font files and format for use by Satori
|
||||
*/
|
||||
export function getFontDefinitions() {
|
||||
return getFontFiles().map(file => {
|
||||
const basename = path.basename(file, path.extname(file))
|
||||
const italic = basename.includes('Italic')
|
||||
const [weight, postScriptName] = basename.split('-')
|
||||
|
||||
return {
|
||||
name: postScriptName,
|
||||
data: readFileSync(file),
|
||||
weight: parseInt(weight),
|
||||
style: italic ? 'italic' : 'normal',
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function formatCount(count: number) {
|
||||
return Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
// `1,953` shouldn't be rounded up to 2k, it should be truncated.
|
||||
// @ts-ignore
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode
|
||||
roundingMode: 'trunc',
|
||||
}).format(count)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function formatDate(date: number | string | Date) {
|
||||
const d = new Date(date)
|
||||
return `${d.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})} at ${d.toLocaleTimeString(undefined, {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
})}`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export const MAT_WIDTH = 12
|
||||
|
||||
export function getWidthWithMat(width: number) {
|
||||
return width + MAT_WIDTH * 2
|
||||
}
|
||||
|
||||
export function computeMatBorderRadius(borderRadius: number) {
|
||||
return borderRadius + MAT_WIDTH / 2
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import {ModerationCause} from '@atproto/api'
|
||||
|
||||
import {CircleInfo} from '../components/icons/CircleInfo.js'
|
||||
import {EyeSlash} from '../components/icons/EyeSlash.js'
|
||||
import {IconProps} from '../components/icons/types.js'
|
||||
import {Warning} from '../components/icons/Warning.js'
|
||||
import {ModeratorData} from '../data/getModeratorData.js'
|
||||
|
||||
export type ModerationCauseInfo = {
|
||||
icon: React.ComponentType<IconProps>
|
||||
name: string
|
||||
description?: string
|
||||
source: string
|
||||
sourceType: 'label' | string
|
||||
sourceDid: string
|
||||
}
|
||||
|
||||
const globalLabelStrings = {
|
||||
'!hide': {
|
||||
name: `Content Blocked`,
|
||||
description: `This content has been hidden by the moderators.`,
|
||||
},
|
||||
'!warn': {
|
||||
name: `Content Warning`,
|
||||
description: `This content has received a general warning from moderators.`,
|
||||
},
|
||||
'!no-unauthenticated': {
|
||||
name: `Sign-in Required`,
|
||||
description: `This user has requested that their content only be shown to signed-in users.`,
|
||||
},
|
||||
porn: {
|
||||
name: `Adult Content`,
|
||||
description: `Explicit sexual images.`,
|
||||
},
|
||||
sexual: {
|
||||
name: `Sexually Suggestive`,
|
||||
description: `Does not include nudity.`,
|
||||
},
|
||||
nudity: {
|
||||
name: `Non-sexual Nudity`,
|
||||
description: `E.g. artistic nudes.`,
|
||||
},
|
||||
'graphic-media': {
|
||||
name: `Graphic Media`,
|
||||
description: `Explicit or potentially disturbing media.`,
|
||||
},
|
||||
}
|
||||
|
||||
export function getModerationCauseInfo({
|
||||
cause,
|
||||
moderatorData,
|
||||
}: {
|
||||
cause?: ModerationCause
|
||||
moderatorData: ModeratorData
|
||||
}): ModerationCauseInfo | undefined {
|
||||
if (!cause) return undefined
|
||||
|
||||
if (cause.type === 'label') {
|
||||
const def = cause.labelDef
|
||||
const {name, description}: {name: string; description: string} =
|
||||
def.locales.find(l => l.lang === 'en') ||
|
||||
globalLabelStrings[def.identifier]
|
||||
const source =
|
||||
moderatorData.labeler.creator.displayName ||
|
||||
'@' + moderatorData.labeler.creator.handle
|
||||
|
||||
return {
|
||||
icon:
|
||||
def.identifier === '!no-unauthenticated'
|
||||
? EyeSlash
|
||||
: def.severity === 'alert'
|
||||
? Warning
|
||||
: CircleInfo,
|
||||
name,
|
||||
description,
|
||||
source,
|
||||
sourceType: cause.source.type,
|
||||
// sourceAvi: labeler?.creator.avatar,
|
||||
sourceDid: cause.label.src,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {AppBskyGraphDefs, AtUri} from '@atproto/api'
|
||||
|
||||
const DEV = process.env.NODE_ENV === 'development'
|
||||
const HOST = DEV
|
||||
? `http://localhost:3000/start`
|
||||
: `https://ogcard.cdn.bsky.app/start`
|
||||
|
||||
export function getStarterPackImageUri(
|
||||
record: AppBskyGraphDefs.StarterPackViewBasic,
|
||||
) {
|
||||
const urip = new AtUri(record.uri)
|
||||
return `${HOST}/${record.creator.did}/${urip.rkey}`
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import {
|
||||
BSKY_LABELER_DID,
|
||||
moderatePost as defaultModeratePost,
|
||||
} from '@atproto/api'
|
||||
|
||||
type ModeratePost = typeof defaultModeratePost
|
||||
export type Subject = Parameters<ModeratePost>[0]
|
||||
export type Options = Parameters<ModeratePost>[1]
|
||||
|
||||
function translateOldLabels(subject: Parameters<ModeratePost>[0]) {
|
||||
if (subject.labels) {
|
||||
for (const label of subject.labels) {
|
||||
if (
|
||||
label.val === 'gore' &&
|
||||
(!label.src || label.src === BSKY_LABELER_DID)
|
||||
) {
|
||||
label.val = 'graphic-media'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function moderatePost(post: Subject, opts: Options) {
|
||||
// HACK
|
||||
// temporarily translate 'gore' into 'graphic-media' during the transition period
|
||||
// can remove this in a few months
|
||||
// -prf
|
||||
translateOldLabels(post)
|
||||
|
||||
return defaultModeratePost(post, opts)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* This file is a copy of what exists in the social-app
|
||||
*/
|
||||
|
||||
import {
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyEmbedVideo,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyLabelerDefs,
|
||||
} from '@atproto/api'
|
||||
|
||||
export type Embed =
|
||||
| {
|
||||
type: 'post'
|
||||
view: AppBskyEmbedRecord.ViewRecord
|
||||
}
|
||||
| {
|
||||
type: 'post_not_found'
|
||||
view: AppBskyEmbedRecord.ViewNotFound
|
||||
}
|
||||
| {
|
||||
type: 'post_blocked'
|
||||
view: AppBskyEmbedRecord.ViewBlocked
|
||||
}
|
||||
| {
|
||||
type: 'post_detached'
|
||||
view: AppBskyEmbedRecord.ViewDetached
|
||||
}
|
||||
| {
|
||||
type: 'feed'
|
||||
view: AppBskyFeedDefs.GeneratorView
|
||||
}
|
||||
| {
|
||||
type: 'list'
|
||||
view: AppBskyGraphDefs.ListView
|
||||
}
|
||||
| {
|
||||
type: 'labeler'
|
||||
view: AppBskyLabelerDefs.LabelerView
|
||||
}
|
||||
| {
|
||||
type: 'starter_pack'
|
||||
view: AppBskyGraphDefs.StarterPackViewBasic
|
||||
}
|
||||
| {
|
||||
type: 'images'
|
||||
view: AppBskyEmbedImages.View
|
||||
}
|
||||
| {
|
||||
type: 'link'
|
||||
view: AppBskyEmbedExternal.View
|
||||
}
|
||||
| {
|
||||
type: 'video'
|
||||
view: AppBskyEmbedVideo.View
|
||||
}
|
||||
| {
|
||||
type: 'post_with_media'
|
||||
view: Embed
|
||||
media: Embed
|
||||
}
|
||||
| {
|
||||
type: 'unknown'
|
||||
view: null
|
||||
}
|
||||
|
||||
export type EmbedType<T extends Embed['type']> = Extract<Embed, {type: T}>
|
||||
|
||||
export function parseEmbedRecordView({record}: AppBskyEmbedRecord.View): Embed {
|
||||
if (AppBskyEmbedRecord.isViewRecord(record)) {
|
||||
return {
|
||||
type: 'post',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isViewNotFound(record)) {
|
||||
return {
|
||||
type: 'post_not_found',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isViewBlocked(record)) {
|
||||
return {
|
||||
type: 'post_blocked',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isViewDetached(record)) {
|
||||
return {
|
||||
type: 'post_detached',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyFeedDefs.isGeneratorView(record)) {
|
||||
return {
|
||||
type: 'feed',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyGraphDefs.isListView(record)) {
|
||||
return {
|
||||
type: 'list',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyLabelerDefs.isLabelerView(record)) {
|
||||
return {
|
||||
type: 'labeler',
|
||||
view: record,
|
||||
}
|
||||
} else if (AppBskyGraphDefs.isStarterPackViewBasic(record)) {
|
||||
return {
|
||||
type: 'starter_pack',
|
||||
view: record,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: 'unknown',
|
||||
view: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseEmbed(embed: AppBskyFeedDefs.PostView['embed']): Embed {
|
||||
if (AppBskyEmbedImages.isView(embed)) {
|
||||
return {
|
||||
type: 'images',
|
||||
view: embed,
|
||||
}
|
||||
} else if (AppBskyEmbedExternal.isView(embed)) {
|
||||
return {
|
||||
type: 'link',
|
||||
view: embed,
|
||||
}
|
||||
} else if (AppBskyEmbedVideo.isView(embed)) {
|
||||
return {
|
||||
type: 'video',
|
||||
view: embed,
|
||||
}
|
||||
} else if (AppBskyEmbedRecord.isView(embed)) {
|
||||
return parseEmbedRecordView(embed)
|
||||
} else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
return {
|
||||
type: 'post_with_media',
|
||||
view: parseEmbedRecordView(embed.record),
|
||||
media: parseEmbed(embed.media),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: 'unknown',
|
||||
view: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
export const embedPlayerSources = ['giphy', 'tenor'] as const
|
||||
|
||||
export type EmbedPlayerSource = (typeof embedPlayerSources)[number]
|
||||
|
||||
export type EmbedPlayerType = 'giphy_gif' | 'tenor_gif'
|
||||
|
||||
export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
|
||||
giphy: 'GIPHY',
|
||||
tenor: 'Tenor',
|
||||
}
|
||||
|
||||
export interface EmbedPlayerParams {
|
||||
type: EmbedPlayerType
|
||||
playerUri: string
|
||||
isGif?: boolean
|
||||
source: EmbedPlayerSource
|
||||
metaUri?: string
|
||||
hideDetails?: boolean
|
||||
dimensions?: {
|
||||
height: number
|
||||
width: number
|
||||
}
|
||||
}
|
||||
|
||||
const giphyRegex = /media(?:[0-4]\.giphy\.com|\.giphy\.com)/i
|
||||
const gifFilenameRegex = /^(\S+)\.(webp|gif|mp4)$/i
|
||||
|
||||
export function parseEmbedPlayerFromUrl(
|
||||
url: string,
|
||||
): EmbedPlayerParams | undefined {
|
||||
let urlp
|
||||
try {
|
||||
urlp = new URL(url)
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (urlp.hostname === 'giphy.com' || urlp.hostname === 'www.giphy.com') {
|
||||
const [_, gifs, nameAndId] = urlp.pathname.split('/')
|
||||
|
||||
/*
|
||||
* nameAndId is a string that consists of the name (dash separated) and the id of the gif (the last part of the name)
|
||||
* We want to get the id of the gif, then direct to media.giphy.com/media/{id}/giphy.webp so we can
|
||||
* use it in an <Image> component
|
||||
*/
|
||||
|
||||
if (gifs === 'gifs' && nameAndId) {
|
||||
const gifId = nameAndId.split('-').pop()
|
||||
|
||||
if (gifId) {
|
||||
return {
|
||||
type: 'giphy_gif',
|
||||
source: 'giphy',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: `https://giphy.com/gifs/${gifId}`,
|
||||
playerUri: `https://i.giphy.com/media/${gifId}/200.webp`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There are five possible hostnames that also can be giphy urls: media.giphy.com and media0-4.giphy.com
|
||||
// These can include (presumably) a tracking id in the path name, so we have to check for that as well
|
||||
if (giphyRegex.test(urlp.hostname)) {
|
||||
// We can link directly to the gif, if its a proper link
|
||||
const [_, media, trackingOrId, idOrFilename, filename] =
|
||||
urlp.pathname.split('/')
|
||||
|
||||
if (media === 'media') {
|
||||
if (idOrFilename && gifFilenameRegex.test(idOrFilename)) {
|
||||
return {
|
||||
type: 'giphy_gif',
|
||||
source: 'giphy',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: `https://giphy.com/gifs/${trackingOrId}`,
|
||||
playerUri: `https://i.giphy.com/media/${trackingOrId}/200.webp`,
|
||||
}
|
||||
} else if (filename && gifFilenameRegex.test(filename)) {
|
||||
return {
|
||||
type: 'giphy_gif',
|
||||
source: 'giphy',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: `https://giphy.com/gifs/${idOrFilename}`,
|
||||
playerUri: `https://i.giphy.com/media/${idOrFilename}/200.webp`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, we should see if it is a link to i.giphy.com. These links don't necessarily end in .gif but can also
|
||||
// be .webp
|
||||
if (urlp.hostname === 'i.giphy.com' || urlp.hostname === 'www.i.giphy.com') {
|
||||
const [_, mediaOrFilename, filename] = urlp.pathname.split('/')
|
||||
|
||||
if (mediaOrFilename === 'media' && filename) {
|
||||
const gifId = filename.split('.')[0]
|
||||
return {
|
||||
type: 'giphy_gif',
|
||||
source: 'giphy',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: `https://giphy.com/gifs/${gifId}`,
|
||||
playerUri: `https://i.giphy.com/media/${gifId}/200.webp`,
|
||||
}
|
||||
} else if (mediaOrFilename) {
|
||||
const gifId = mediaOrFilename.split('.')[0]
|
||||
return {
|
||||
type: 'giphy_gif',
|
||||
source: 'giphy',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: `https://giphy.com/gifs/${gifId}`,
|
||||
playerUri: `https://i.giphy.com/media/${
|
||||
mediaOrFilename.split('.')[0]
|
||||
}/200.webp`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tenorGif = parseTenorGif(urlp)
|
||||
if (tenorGif.success) {
|
||||
const {playerUri, dimensions} = tenorGif
|
||||
|
||||
return {
|
||||
type: 'tenor_gif',
|
||||
source: 'tenor',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
playerUri,
|
||||
dimensions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getGifDims(
|
||||
originalHeight: number,
|
||||
originalWidth: number,
|
||||
viewWidth: number,
|
||||
) {
|
||||
const scaledHeight = (originalHeight / originalWidth) * viewWidth
|
||||
|
||||
return {
|
||||
height: scaledHeight > 250 ? 250 : scaledHeight,
|
||||
width: (250 / scaledHeight) * viewWidth,
|
||||
}
|
||||
}
|
||||
|
||||
export function getGiphyMetaUri(url: URL) {
|
||||
if (giphyRegex.test(url.hostname) || url.hostname === 'i.giphy.com') {
|
||||
const params = parseEmbedPlayerFromUrl(url.toString())
|
||||
if (params && params.type === 'giphy_gif') {
|
||||
return params.metaUri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseTenorGif(urlp: URL):
|
||||
| {success: false}
|
||||
| {
|
||||
success: true
|
||||
playerUri: string
|
||||
dimensions: {height: number; width: number}
|
||||
} {
|
||||
if (urlp.hostname !== 'media.tenor.com') {
|
||||
return {success: false}
|
||||
}
|
||||
|
||||
let [_, id, filename] = urlp.pathname.split('/')
|
||||
|
||||
if (!id || !filename) {
|
||||
return {success: false}
|
||||
}
|
||||
|
||||
if (!id.includes('AAAAC')) {
|
||||
return {success: false}
|
||||
}
|
||||
|
||||
const h = urlp.searchParams.get('hh')
|
||||
const w = urlp.searchParams.get('ww')
|
||||
|
||||
if (!h || !w) {
|
||||
return {success: false}
|
||||
}
|
||||
|
||||
const dimensions = {
|
||||
height: Number(h),
|
||||
width: Number(w),
|
||||
}
|
||||
|
||||
// if (isWeb) {
|
||||
// id = id.replace('AAAAC', 'AAAP3')
|
||||
// filename = filename.replace('.gif', '.webm')
|
||||
// } else {
|
||||
id = id.replace('AAAAC', 'AAAAM')
|
||||
// }
|
||||
|
||||
return {
|
||||
success: true,
|
||||
playerUri: `https://t.gifs.bsky.app/${id}/${filename}`,
|
||||
dimensions,
|
||||
}
|
||||
}
|
||||
|
||||
export function isTenorGifUri(url: URL | string) {
|
||||
try {
|
||||
return parseTenorGif(typeof url === 'string' ? new URL(url) : url).success
|
||||
} catch {
|
||||
// Invalid URL
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {ParsedQs} from 'qs'
|
||||
|
||||
import {getWidthWithMat} from './frameMatting.js'
|
||||
|
||||
export const DEFAULT_WIDTH = 360
|
||||
|
||||
/**
|
||||
* Options parsed from the query string, passed to the components themselves.
|
||||
*/
|
||||
export type DisplayOptions = {
|
||||
mat: boolean
|
||||
}
|
||||
|
||||
export function parseDisplayOptionsFromQuery(query: ParsedQs): DisplayOptions {
|
||||
const opts: DisplayOptions = {
|
||||
mat: false,
|
||||
}
|
||||
|
||||
;['mat'].forEach(key => {
|
||||
const val = query[key]
|
||||
if (val === 'true' || val === '1') {
|
||||
opts[key] = true
|
||||
} else {
|
||||
opts[key] = false
|
||||
}
|
||||
})
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
/**
|
||||
* Options computed from the parsed query options, passed to the render
|
||||
* function.
|
||||
*/
|
||||
export function getRenderOptions(options: DisplayOptions) {
|
||||
return {
|
||||
// must match width + padding of Post.tsx
|
||||
width: options.mat ? getWidthWithMat(DEFAULT_WIDTH) : DEFAULT_WIDTH,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function isInvalidHandle(handle: string): boolean {
|
||||
return handle === 'handle.invalid'
|
||||
}
|
||||
|
||||
export function sanitizeHandle(handle: string, prefix = ''): string {
|
||||
return isInvalidHandle(handle) ? '⚠Invalid Handle' : `${prefix}${handle}`
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export const BSKY_SERVICE = 'https://bsky.social'
|
||||
|
||||
export function toNiceDomain(url: string): string {
|
||||
try {
|
||||
const urlp = new URL(url)
|
||||
if (`https://${urlp.host}` === BSKY_SERVICE) {
|
||||
return 'Bluesky Social'
|
||||
}
|
||||
return urlp.host ? urlp.host : url
|
||||
} catch (e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export function toShortUrl(url: string): string {
|
||||
try {
|
||||
const urlp = new URL(url)
|
||||
if (urlp.protocol !== 'http:' && urlp.protocol !== 'https:') {
|
||||
return url
|
||||
}
|
||||
const path =
|
||||
(urlp.pathname === '/' ? '' : urlp.pathname) + urlp.search + urlp.hash
|
||||
if (path.length > 15) {
|
||||
return urlp.host + path.slice(0, 13) + '...'
|
||||
}
|
||||
return urlp.host + path
|
||||
} catch (e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {useMemo} from 'react'
|
||||
|
||||
import * as bsky from '../types/bsky/index.js'
|
||||
|
||||
export type VerificationState = {
|
||||
role: 'default' | 'verifier'
|
||||
isVerified: boolean
|
||||
}
|
||||
|
||||
export function getVerificationState({
|
||||
profile,
|
||||
}: {
|
||||
profile?: bsky.profile.AnyProfileView
|
||||
}): VerificationState {
|
||||
if (!profile || !profile.verification) {
|
||||
return {
|
||||
role: 'default',
|
||||
isVerified: false,
|
||||
}
|
||||
}
|
||||
|
||||
const {verifiedStatus, trustedVerifierStatus} = profile.verification
|
||||
const isVerifiedUser = ['valid', 'invalid'].includes(verifiedStatus)
|
||||
const isVerifierUser = ['valid', 'invalid'].includes(trustedVerifierStatus)
|
||||
const isVerified =
|
||||
(isVerifiedUser && verifiedStatus === 'valid') ||
|
||||
(isVerifierUser && trustedVerifierStatus === 'valid')
|
||||
|
||||
return {
|
||||
role: isVerifierUser ? 'verifier' : 'default',
|
||||
isVerified,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {AppBskyEmbedRecord, AppBskyFeedDefs} from '@atproto/api'
|
||||
|
||||
export function viewRecordToPostView(
|
||||
viewRecord: AppBskyEmbedRecord.ViewRecord,
|
||||
): AppBskyFeedDefs.PostView {
|
||||
const {value, embeds, ...rest} = viewRecord
|
||||
return {
|
||||
...rest,
|
||||
$type: 'app.bsky.feed.defs#postView',
|
||||
record: value,
|
||||
embed: embeds?.[0],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/svbstrate@5.1.0/svbstrate.css" />
|
||||
<style>
|
||||
body {
|
||||
padding: 2rem;
|
||||
}
|
||||
section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
.item {
|
||||
text-align: center;
|
||||
}
|
||||
.item img {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
display: block;
|
||||
margin: 0 auto 0.5rem;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="p12">
|
||||
|
||||
<!-- Base embeds -->
|
||||
<section>
|
||||
<h1>Base embeds</h1>
|
||||
<div class="grid">
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkch7rnmqs23?mat=1"><div>Image</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchac5lbs23?mat=1"><div>Image w/ label</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchkcokw22h?mat=1"><div>Image x2</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkjhqtunc22v?mat=1"><div>Image x3</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkjhsgb4vk2z?mat=1"><div>Image x4</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchd5laes23?mat=1"><div>Video</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchdnqmzc23?mat=1"><div>Video w/ label</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchef5c2c23?mat=1"><div>GIF</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchgiinq22h?mat=1"><div>List</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchgwpy222h?mat=1"><div>List w/ label</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchhfft722h?mat=1"><div>List (takendown)</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchj6t27k2h?mat=1"><div>Feed</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkjid3khps2r?mat=1"><div>Feed w/ label</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchlshes22h?mat=1"><div>Link</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchm7qo622h?mat=1"><div>Link w/ label</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchnlwlbs2h?mat=1"><div>Starter pack</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/techcrunch.com/post/3lcnpwhz4d22j?mat=1"><div>Starter pack w/ signups</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quote posts -->
|
||||
<section>
|
||||
<h1>Quote posts</h1>
|
||||
<div class="grid">
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchp7oaq22h?mat=1"><div>Image</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchpijwu22h?mat=1"><div>Image w/ label</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchpvu6is2h?mat=1"><div>Video</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchq5dfrk2h?mat=1"><div>Video w/ label</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkjip5qduk2l?mat=1"><div>GIF</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchqdm3nk2h?mat=1"><div>List</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchqmp2722h?mat=1"><div>List (takendown)</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchqrrigs2h?mat=1"><div>Feed</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchqyf6u22h?mat=1"><div>Link</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchremdhc2h?mat=1"><div>Link w/ label</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkchrlw4vk2h?mat=1"><div>Starter pack</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Quote post states -->
|
||||
<section>
|
||||
<h1>Quote post states</h1>
|
||||
<div class="grid">
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkcieaeil22a?mat=1"><div>Deleted</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkciefiepk2a?mat=1"><div>Detached quoted post</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/test-all-embeds-a.bsky.social/post/3lkcikkheys2c?mat=1"><div>Blocked</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Default avatars -->
|
||||
<section>
|
||||
<h1>Default avatars</h1>
|
||||
<div class="grid">
|
||||
<div class="item"><img src="http://localhost:3000/profile/bleh-5.bsky.social/post/3lr6qski2i22o?mat=1" /><div>Default avatar</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/bleh-5.bsky.social/post/3lr6qrk74rc2o?mat=1" /><div>Default avi quote</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/bleh-5.bsky.social/post/3lr6qsegoy22o?mat=1" /><div>Default avi list</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/bleh-5.bsky.social/post/3lr6qy3dpu22o?mat=1" /><div>Default avi feed</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Verification -->
|
||||
<section>
|
||||
<h1>Verification</h1>
|
||||
<div class="grid">
|
||||
<div class="item"><img src="http://localhost:3000/profile/wired.com/post/3lr7im23gjr2f?mat=1" /><div>Trusted verifier</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/bastardsheep.com/post/3lr7jf37gs42v?mat=1" /><div>Trusted verifier quoted</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/julianchokkattu.bsky.social/post/3lr7jbcxpsc2u?mat=1" /><div>Verified</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/retr0.id/post/3locfcr4tv22c?mat=1" /><div>Verified quoted</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Other languages -->
|
||||
<section>
|
||||
<h1>Other languages</h1>
|
||||
<div class="grid">
|
||||
<div class="item"><img src="http://localhost:3000/profile/miyanushi.bsky.social/post/3lrqt3xxmuk2a?mat=1"><div>Japanese</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/amir.blue/post/3lbad2u6zg22a?mat=1"><div>Hebrew</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Misc -->
|
||||
<section>
|
||||
<h1>Misc</h1>
|
||||
<div class="grid">
|
||||
<div class="item"><img src="http://localhost:3000/profile/xblock.aendra.dev/post/3ljo3ne5cxk2h?mat=1"><div>Post from labeler</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/hailey5.bsky.social/post/3kl4psn4zbk2q?mat=1"><div>Long name</div></div>
|
||||
<div class="item"><img src="http://localhost:3000/profile/deepfates.com.deepfates.com.deepfates.com.deepfates.com.deepfates.com/post/3jvcrxfpuhy2i?mat=1"><div>Long handle</div></div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
<!-- reload image on click -->
|
||||
<script>
|
||||
document.querySelectorAll('.item img').forEach(img => {
|
||||
img.onload = () => img.style.opacity = 1;
|
||||
img.addEventListener('click', () => {
|
||||
img.style.opacity = 0.5;
|
||||
const url = new URL(img.src);
|
||||
url.searchParams.set('t', Date.now()); // bust the cache
|
||||
img.src = url.toString();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
@@ -5,7 +5,8 @@
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "NodeNext",
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "dist"
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./src/index.ts", "./src/bin.ts"]
|
||||
}
|
||||
|
||||
+589
-42
@@ -2,63 +2,72 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@atproto/api@0.12.19-next.0":
|
||||
version "0.12.19-next.0"
|
||||
resolved "https://registry.npmjs.org/@atproto/api/-/api-0.12.19-next.0.tgz"
|
||||
integrity sha512-wyWr4uIabTgDTBY99y3QyrFxcIx1Mh4DkURgSv8sd/b+w0lfrZAJh0Gg9BXdg/iIjcf/M2lCTL04r0vASfkMVg==
|
||||
"@atproto/api@^0.15.14":
|
||||
version "0.15.14"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.14.tgz#41ff6ce2e7603119a005b7b5ce8e64551ec84879"
|
||||
integrity sha512-FHEMAdscG+r2OFcZUIzPyTDpwzRAyinRsIIaTcuqe0MgZWF4CEGNAKPos0IbecBzMxTOzUHE18dQDKhoXMdgvg==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/lexicon" "^0.4.0"
|
||||
"@atproto/syntax" "^0.3.0"
|
||||
"@atproto/xrpc" "^0.5.0"
|
||||
"@atproto/common-web" "^0.4.2"
|
||||
"@atproto/lexicon" "^0.4.11"
|
||||
"@atproto/syntax" "^0.4.0"
|
||||
"@atproto/xrpc" "^0.7.0"
|
||||
await-lock "^2.2.2"
|
||||
multiformats "^9.9.0"
|
||||
tlds "^1.234.0"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/common-web@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.0.tgz"
|
||||
integrity sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA==
|
||||
"@atproto/common-web@^0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.2.tgz#6e3add6939da93d3dfbc8f87e26dc4f57fad7259"
|
||||
integrity sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==
|
||||
dependencies:
|
||||
graphemer "^1.4.0"
|
||||
multiformats "^9.9.0"
|
||||
uint8arrays "3.0.0"
|
||||
zod "^3.21.4"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/common@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/@atproto/common/-/common-0.4.0.tgz"
|
||||
integrity sha512-yOXuPlCjT/OK9j+neIGYn9wkxx/AlxQSucysAF0xgwu0Ji8jAtKBf9Jv6R5ObYAjAD/kVUvEYumle+Yq/R9/7g==
|
||||
"@atproto/common@^0.4.11":
|
||||
version "0.4.11"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.11.tgz#9291b7c26f8b3507e280f7ecbdf1695ab5ea62f6"
|
||||
integrity sha512-Knv0viYXNMfCdIE7jLUiWJKnnMfEwg+vz2epJQi8WOjqtqCFb3W/3Jn72ZiuovIfpdm13MaOiny6w2NErUQC6g==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/common-web" "^0.4.2"
|
||||
"@ipld/dag-cbor" "^7.0.3"
|
||||
cbor-x "^1.5.1"
|
||||
iso-datestring-validator "^2.2.2"
|
||||
multiformats "^9.9.0"
|
||||
pino "^8.15.0"
|
||||
pino "^8.21.0"
|
||||
|
||||
"@atproto/lexicon@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.0.tgz"
|
||||
integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ==
|
||||
"@atproto/lexicon@^0.4.11":
|
||||
version "0.4.11"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.11.tgz#d5d09be1faf1d28d1e57051dab4064101f8b1617"
|
||||
integrity sha512-btefdnvNz2Ao2I+qbmj0F06HC8IlrM/IBz6qOBS50r0S6uDf5tOO+Mv2tSVdimFkdzyDdLtBI1sV36ONxz2cOw==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/syntax" "^0.3.0"
|
||||
"@atproto/common-web" "^0.4.2"
|
||||
"@atproto/syntax" "^0.4.0"
|
||||
iso-datestring-validator "^2.2.2"
|
||||
multiformats "^9.9.0"
|
||||
zod "^3.21.4"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@atproto/syntax@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.0.tgz"
|
||||
integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==
|
||||
"@atproto/syntax@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.4.0.tgz#bec71552087bb24c208a06ef418c0040b65542f2"
|
||||
integrity sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA==
|
||||
|
||||
"@atproto/xrpc@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.5.0.tgz"
|
||||
integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==
|
||||
"@atproto/xrpc@^0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.0.tgz#7d1e497d682431fecd7085d7482e83d8a33821b0"
|
||||
integrity sha512-SfhP9dGx2qclaScFDb58Jnrmim5nk4geZXCqg6sB0I/KZhZEkr9iIx1hLCp+sxkIfEsmEJjeWO4B0rjUIJW5cw==
|
||||
dependencies:
|
||||
"@atproto/lexicon" "^0.4.0"
|
||||
zod "^3.21.4"
|
||||
"@atproto/lexicon" "^0.4.11"
|
||||
zod "^3.23.8"
|
||||
|
||||
"@bsky.app/alf@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@bsky.app/alf/-/alf-0.1.0.tgz#7fcd82fbb961a3cb675892d1d6568105ea879492"
|
||||
integrity sha512-uw3ga4qfDA+5BijLk1FAJQVBXHIQIwROfLuzvXBTM4oY5gd74cMpvsftvFdwlm7+U+jqsKDqahTAbOM1DXCUiw==
|
||||
dependencies:
|
||||
react-responsive "^10.0.1"
|
||||
|
||||
"@cbor-extract/cbor-extract-darwin-arm64@2.2.0":
|
||||
version "2.2.0"
|
||||
@@ -105,6 +114,93 @@
|
||||
cborg "^1.6.0"
|
||||
multiformats "^9.5.4"
|
||||
|
||||
"@jimp/bmp@^0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.22.12.tgz#0316044dc7b1a90274aef266d50349347fb864d4"
|
||||
integrity sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==
|
||||
dependencies:
|
||||
"@jimp/utils" "^0.22.12"
|
||||
bmp-js "^0.1.0"
|
||||
|
||||
"@jimp/core@^0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@jimp/core/-/core-0.22.12.tgz#70785ea7d10b138fb65bcfe9f712826f00a10e1d"
|
||||
integrity sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==
|
||||
dependencies:
|
||||
"@jimp/utils" "^0.22.12"
|
||||
any-base "^1.1.0"
|
||||
buffer "^5.2.0"
|
||||
exif-parser "^0.1.12"
|
||||
file-type "^16.5.4"
|
||||
isomorphic-fetch "^3.0.0"
|
||||
pixelmatch "^4.0.2"
|
||||
tinycolor2 "^1.6.0"
|
||||
|
||||
"@jimp/custom@^0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@jimp/custom/-/custom-0.22.12.tgz#236f2a3f016b533c50869ff22ad1ac00dd0c36be"
|
||||
integrity sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==
|
||||
dependencies:
|
||||
"@jimp/core" "^0.22.12"
|
||||
|
||||
"@jimp/gif@^0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@jimp/gif/-/gif-0.22.12.tgz#6caccb45df497fb971b7a88690345596e22163c0"
|
||||
integrity sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg==
|
||||
dependencies:
|
||||
"@jimp/utils" "^0.22.12"
|
||||
gifwrap "^0.10.1"
|
||||
omggif "^1.0.9"
|
||||
|
||||
"@jimp/jpeg@^0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@jimp/jpeg/-/jpeg-0.22.12.tgz#b5c74a5aac9826245311370dda8c71a1fcca05ed"
|
||||
integrity sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q==
|
||||
dependencies:
|
||||
"@jimp/utils" "^0.22.12"
|
||||
jpeg-js "^0.4.4"
|
||||
|
||||
"@jimp/plugin-resize@^0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz#f92acbf73beb97dd1fe93b166ef367a323b81e81"
|
||||
integrity sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==
|
||||
dependencies:
|
||||
"@jimp/utils" "^0.22.12"
|
||||
|
||||
"@jimp/png@^0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@jimp/png/-/png-0.22.12.tgz#e033586caf38d9c9d33808e92eb87c4d7f0aa1eb"
|
||||
integrity sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg==
|
||||
dependencies:
|
||||
"@jimp/utils" "^0.22.12"
|
||||
pngjs "^6.0.0"
|
||||
|
||||
"@jimp/tiff@^0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@jimp/tiff/-/tiff-0.22.12.tgz#67cac3f2ded6fde3ef631fbf74bea0fa53800123"
|
||||
integrity sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg==
|
||||
dependencies:
|
||||
utif2 "^4.0.1"
|
||||
|
||||
"@jimp/types@^0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@jimp/types/-/types-0.22.12.tgz#6f83761ba171cb8cd5998fa66a5cbfb0b22d3d8c"
|
||||
integrity sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA==
|
||||
dependencies:
|
||||
"@jimp/bmp" "^0.22.12"
|
||||
"@jimp/gif" "^0.22.12"
|
||||
"@jimp/jpeg" "^0.22.12"
|
||||
"@jimp/png" "^0.22.12"
|
||||
"@jimp/tiff" "^0.22.12"
|
||||
timm "^1.6.1"
|
||||
|
||||
"@jimp/utils@^0.22.12":
|
||||
version "0.22.12"
|
||||
resolved "https://registry.yarnpkg.com/@jimp/utils/-/utils-0.22.12.tgz#8ffaed8f2dc2962539ccaf14727ac60793c7a537"
|
||||
integrity sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.3"
|
||||
|
||||
"@jridgewell/resolve-uri@^3.0.3":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
|
||||
@@ -209,6 +305,11 @@
|
||||
fflate "^0.7.3"
|
||||
string.prototype.codepointat "^0.2.1"
|
||||
|
||||
"@tokenizer/token@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
|
||||
integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
|
||||
|
||||
"@tsconfig/node10@^1.0.7":
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"
|
||||
@@ -229,6 +330,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||
|
||||
"@types/node@16.9.1":
|
||||
version "16.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
|
||||
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
|
||||
|
||||
"@types/node@^18.15.3":
|
||||
version "18.19.80"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.80.tgz#6d6008e8920dddcd23f9dd33da24684ef57d487c"
|
||||
integrity sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/node@^20.14.3":
|
||||
version "20.14.3"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-20.14.3.tgz"
|
||||
@@ -236,6 +349,92 @@
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@vibrant/color@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/color/-/color-4.0.0.tgz#f804da47caf0072bc9bafddd94aa42650b0d1e25"
|
||||
integrity sha512-S9ItdqS1135wTXoIIqAJu8df9dqlOo6Boc5Y4MGsBTu9UmUOvOwfj5b4Ga6S5yrLAKmKYIactkz7zYJdMddkig==
|
||||
|
||||
"@vibrant/core@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/core/-/core-4.0.0.tgz#a13a56b223c7f12543773b8d8ba2263096c6f5ce"
|
||||
integrity sha512-fqlVRUTDjEws9VNKvI3cDXM4wUT7fMFS+cVqEjJk3im+R5EvjJzPF6OAbNhfPzW04NvHNE555eY9FfhYuX3PRw==
|
||||
dependencies:
|
||||
"@vibrant/color" "^4.0.0"
|
||||
"@vibrant/generator" "^4.0.0"
|
||||
"@vibrant/image" "^4.0.0"
|
||||
"@vibrant/quantizer" "^4.0.0"
|
||||
"@vibrant/worker" "^4.0.0"
|
||||
|
||||
"@vibrant/generator-default@^4.0.3":
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/generator-default/-/generator-default-4.0.3.tgz#2634a4c0b17698515666bed993100d6b85e9eef9"
|
||||
integrity sha512-HZlfp19sDokODEkZF4p70QceARHgjP3a1Dmxg+dlblYMJM98jPq+azA0fzqKNR7R17JJNHxexpJEepEsNlG0gw==
|
||||
dependencies:
|
||||
"@vibrant/color" "^4.0.0"
|
||||
"@vibrant/generator" "^4.0.0"
|
||||
|
||||
"@vibrant/generator@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/generator/-/generator-4.0.0.tgz#2fad5054c76695750d32c88056ab0bd823733c3e"
|
||||
integrity sha512-CqKAjmgHVDXJVo3Q5+9pUJOvksR7cN3bzx/6MbURYh7lA4rhsIewkUK155M6q0vfcUN3ETi/eTneCi0tLuM2Sg==
|
||||
dependencies:
|
||||
"@vibrant/color" "^4.0.0"
|
||||
"@vibrant/types" "^4.0.0"
|
||||
|
||||
"@vibrant/image-browser@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/image-browser/-/image-browser-4.0.0.tgz#9f62901ebbbf147b7c00aae84b5965479731f1b7"
|
||||
integrity sha512-mXckzvJWiP575Y/wNtP87W/TPgyJoGlPBjW4E9YmNS6n4Jb6RqyHQA0ZVulqDslOxjSsihDzY7gpAORRclaoLg==
|
||||
dependencies:
|
||||
"@vibrant/image" "^4.0.0"
|
||||
|
||||
"@vibrant/image-node@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/image-node/-/image-node-4.0.0.tgz#3d60221f97215eebd5a7d6c34d563b7992da1e94"
|
||||
integrity sha512-m7yfnQtmo2y8z+tOjRFBx6q/qGnhl/ax2uCaj4TBkm4TtXfR4Dsn90wT6OWXmCFFzxIKHXKKEBShkxR+4RHseA==
|
||||
dependencies:
|
||||
"@jimp/custom" "^0.22.12"
|
||||
"@jimp/plugin-resize" "^0.22.12"
|
||||
"@jimp/types" "^0.22.12"
|
||||
"@vibrant/image" "^4.0.0"
|
||||
|
||||
"@vibrant/image@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/image/-/image-4.0.0.tgz#2c16047a760d5969c59c5686981a972b0fb37c4d"
|
||||
integrity sha512-Asv/7R/L701norosgvbjOVkodFiwcFihkXixA/gbAd6C+5GCts1Wm1NPk14FNKnM7eKkfAN+0wwPkdOH+PY/YA==
|
||||
dependencies:
|
||||
"@vibrant/color" "^4.0.0"
|
||||
|
||||
"@vibrant/quantizer-mmcq@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/quantizer-mmcq/-/quantizer-mmcq-4.0.0.tgz#b82469653d21827233dbc243b7cebc3d4b761cd9"
|
||||
integrity sha512-TZqNiRoGGyCP8fH1XE6rvhFwLNv9D8MP1Xhz3K8tsuUweC6buWax3qLfrfEnkhtQnPJHaqvTfTOlIIXVMfRpow==
|
||||
dependencies:
|
||||
"@vibrant/color" "^4.0.0"
|
||||
"@vibrant/image" "^4.0.0"
|
||||
"@vibrant/quantizer" "^4.0.0"
|
||||
|
||||
"@vibrant/quantizer@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/quantizer/-/quantizer-4.0.0.tgz#48cb29feca2cea98c1129ce46f85995a8e206462"
|
||||
integrity sha512-YDGxmCv/RvHFtZghDlVRwH5GMxdGGozWS1JpUOUt73/F5zAKGiiier8F31K1npIXARn6/Gspvg/Rbg7qqyEr2A==
|
||||
dependencies:
|
||||
"@vibrant/color" "^4.0.0"
|
||||
"@vibrant/image" "^4.0.0"
|
||||
"@vibrant/types" "^4.0.0"
|
||||
|
||||
"@vibrant/types@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/types/-/types-4.0.0.tgz#8c5a777d827d5df986644abe5c05d16b4114012f"
|
||||
integrity sha512-tA5TAbuROXcPkt+PWjmGfoaiEXyySVaNnCZovf6vXhCbMdrTTCQXvNCde2geiVl6YwtuU/Qrj9iZxS5jZ6yVIw==
|
||||
|
||||
"@vibrant/worker@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vibrant/worker/-/worker-4.0.0.tgz#ca60568f5b607af5786be9ce625efc397087fbdb"
|
||||
integrity sha512-nSaZZwWQKOgN/nPYUAIRF0/uoa7KpK91A+gjLmZZDgfN1enqxaiihmn+75ayNadW0c6cxAEpEFEHTONR5u9tMw==
|
||||
dependencies:
|
||||
"@vibrant/types" "^4.0.0"
|
||||
|
||||
abort-controller@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz"
|
||||
@@ -263,6 +462,11 @@ acorn@^8.11.0, acorn@^8.4.1:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c"
|
||||
integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==
|
||||
|
||||
any-base@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/any-base/-/any-base-1.1.0.tgz#ae101a62bc08a597b4c9ab5b7089d456630549fe"
|
||||
integrity sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
@@ -278,6 +482,11 @@ atomic-sleep@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz"
|
||||
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
|
||||
|
||||
await-lock@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
|
||||
integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
|
||||
|
||||
base64-js@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz"
|
||||
@@ -288,6 +497,11 @@ base64-js@^1.3.1:
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
bmp-js@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233"
|
||||
integrity sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==
|
||||
|
||||
body-parser@1.20.2:
|
||||
version "1.20.2"
|
||||
resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz"
|
||||
@@ -311,6 +525,14 @@ boolean@^3.1.4:
|
||||
resolved "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz"
|
||||
integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
|
||||
|
||||
buffer@^5.2.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
buffer@^6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
|
||||
@@ -371,6 +593,11 @@ color-name@^1.1.4:
|
||||
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
colorette@^2.0.7:
|
||||
version "2.0.20"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
||||
|
||||
content-disposition@0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz"
|
||||
@@ -413,6 +640,11 @@ css-color-keywords@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz"
|
||||
integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==
|
||||
|
||||
css-mediaquery@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/css-mediaquery/-/css-mediaquery-0.1.2.tgz#6a2c37344928618631c54bd33cedd301da18bea0"
|
||||
integrity sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==
|
||||
|
||||
css-to-react-native@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz"
|
||||
@@ -422,6 +654,11 @@ css-to-react-native@^3.0.0:
|
||||
css-color-keywords "^1.0.0"
|
||||
postcss-value-parser "^4.0.2"
|
||||
|
||||
dateformat@^4.6.3:
|
||||
version "4.6.3"
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
|
||||
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
|
||||
|
||||
debug@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
|
||||
@@ -478,6 +715,13 @@ encodeurl@~1.0.2:
|
||||
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz"
|
||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
es-define-property@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz"
|
||||
@@ -510,6 +754,11 @@ events@^3.3.0:
|
||||
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
|
||||
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
||||
|
||||
exif-parser@^0.1.12:
|
||||
version "0.1.12"
|
||||
resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922"
|
||||
integrity sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==
|
||||
|
||||
express@^4.19.2:
|
||||
version "4.19.2"
|
||||
resolved "https://registry.npmjs.org/express/-/express-4.19.2.tgz"
|
||||
@@ -547,6 +796,11 @@ express@^4.19.2:
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
fast-copy@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35"
|
||||
integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==
|
||||
|
||||
fast-printf@^1.6.9:
|
||||
version "1.6.9"
|
||||
resolved "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.9.tgz"
|
||||
@@ -559,11 +813,25 @@ fast-redact@^3.1.1:
|
||||
resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz"
|
||||
integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==
|
||||
|
||||
fast-safe-stringify@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||
|
||||
fflate@^0.7.3:
|
||||
version "0.7.4"
|
||||
resolved "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz"
|
||||
integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==
|
||||
|
||||
file-type@^16.5.4:
|
||||
version "16.5.4"
|
||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd"
|
||||
integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==
|
||||
dependencies:
|
||||
readable-web-to-node-stream "^3.0.0"
|
||||
strtok3 "^6.2.4"
|
||||
token-types "^4.1.1"
|
||||
|
||||
finalhandler@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz"
|
||||
@@ -612,6 +880,14 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
|
||||
has-symbols "^1.0.3"
|
||||
hasown "^2.0.0"
|
||||
|
||||
gifwrap@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/gifwrap/-/gifwrap-0.10.1.tgz#9ed46a5d51913b482d4221ce9c727080260b681e"
|
||||
integrity sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==
|
||||
dependencies:
|
||||
image-q "^4.0.0"
|
||||
omggif "^1.0.10"
|
||||
|
||||
gopd@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz"
|
||||
@@ -653,6 +929,11 @@ hasown@^2.0.0:
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
help-me@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6"
|
||||
integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==
|
||||
|
||||
hex-rgb@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz"
|
||||
@@ -679,6 +960,11 @@ http-terminator@^3.2.0:
|
||||
roarr "^7.0.4"
|
||||
type-fest "^2.3.3"
|
||||
|
||||
hyphenate-style-name@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz#1797bf50369588b47b72ca6d5e65374607cf4436"
|
||||
integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz"
|
||||
@@ -686,11 +972,18 @@ iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
ieee754@^1.2.1:
|
||||
ieee754@^1.1.13, ieee754@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
image-q@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/image-q/-/image-q-4.0.0.tgz#31e075be7bae3c1f42a85c469b4732c358981776"
|
||||
integrity sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==
|
||||
dependencies:
|
||||
"@types/node" "16.9.1"
|
||||
|
||||
inherits@2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
||||
@@ -706,6 +999,24 @@ iso-datestring-validator@^2.2.2:
|
||||
resolved "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz"
|
||||
integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==
|
||||
|
||||
isomorphic-fetch@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4"
|
||||
integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==
|
||||
dependencies:
|
||||
node-fetch "^2.6.1"
|
||||
whatwg-fetch "^3.4.1"
|
||||
|
||||
joycon@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
|
||||
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
|
||||
|
||||
jpeg-js@^0.4.4:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
|
||||
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
||||
@@ -735,7 +1046,7 @@ linebreak@^1.1.0:
|
||||
base64-js "0.0.8"
|
||||
unicode-trie "^2.0.0"
|
||||
|
||||
loose-envify@^1.1.0:
|
||||
loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
@@ -747,6 +1058,13 @@ make-error@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
|
||||
matchmediaquery@^0.4.2:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/matchmediaquery/-/matchmediaquery-0.4.2.tgz#22582bd4ae63ad9f54c53001bba80cbed0f7eafa"
|
||||
integrity sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==
|
||||
dependencies:
|
||||
css-mediaquery "^0.1.2"
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
|
||||
@@ -779,6 +1097,11 @@ mime@1.6.0:
|
||||
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
|
||||
@@ -799,6 +1122,13 @@ negotiator@0.6.3:
|
||||
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
|
||||
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-gyp-build-optional-packages@5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz"
|
||||
@@ -806,11 +1136,33 @@ node-gyp-build-optional-packages@5.1.1:
|
||||
dependencies:
|
||||
detect-libc "^2.0.1"
|
||||
|
||||
node-vibrant@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/node-vibrant/-/node-vibrant-4.0.3.tgz#0737480f2eaceee6336915388541d04f6af308c7"
|
||||
integrity sha512-kzoIuJK90BH/k65Avt077JCX4Nhqz1LNc8cIOm2rnYEvFdJIYd8b3SQwU1MTpzcHtr8z8jxkl1qdaCfbP3olFg==
|
||||
dependencies:
|
||||
"@types/node" "^18.15.3"
|
||||
"@vibrant/core" "^4.0.0"
|
||||
"@vibrant/generator-default" "^4.0.3"
|
||||
"@vibrant/image-browser" "^4.0.0"
|
||||
"@vibrant/image-node" "^4.0.0"
|
||||
"@vibrant/quantizer-mmcq" "^4.0.0"
|
||||
|
||||
object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||
|
||||
object-inspect@^1.13.1:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz"
|
||||
integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
|
||||
|
||||
omggif@^1.0.10, omggif@^1.0.9:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19"
|
||||
integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==
|
||||
|
||||
on-exit-leak-free@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz"
|
||||
@@ -823,6 +1175,13 @@ on-finished@2.4.1:
|
||||
dependencies:
|
||||
ee-first "1.1.1"
|
||||
|
||||
once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
p-finally@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz"
|
||||
@@ -847,6 +1206,11 @@ pako@^0.2.5:
|
||||
resolved "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz"
|
||||
integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==
|
||||
|
||||
pako@^1.0.11:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||
|
||||
parse-css-color@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz"
|
||||
@@ -865,7 +1229,12 @@ path-to-regexp@0.1.7:
|
||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
|
||||
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
|
||||
|
||||
pino-abstract-transport@^1.2.0:
|
||||
peek-readable@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72"
|
||||
integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==
|
||||
|
||||
pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz"
|
||||
integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==
|
||||
@@ -873,6 +1242,26 @@ pino-abstract-transport@^1.2.0:
|
||||
readable-stream "^4.0.0"
|
||||
split2 "^4.0.0"
|
||||
|
||||
pino-pretty@^11.2.2:
|
||||
version "11.2.2"
|
||||
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.2.2.tgz#5e8ec69b31e90eb187715af07b1d29a544e60d39"
|
||||
integrity sha512-2FnyGir8nAJAqD3srROdrF1J5BIcMT4nwj7hHSc60El6Uxlym00UbCCd8pYIterstVBFlMyF1yFV8XdGIPbj4A==
|
||||
dependencies:
|
||||
colorette "^2.0.7"
|
||||
dateformat "^4.6.3"
|
||||
fast-copy "^3.0.2"
|
||||
fast-safe-stringify "^2.1.1"
|
||||
help-me "^5.0.0"
|
||||
joycon "^3.1.1"
|
||||
minimist "^1.2.6"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^1.0.0"
|
||||
pump "^3.0.0"
|
||||
readable-stream "^4.0.0"
|
||||
secure-json-parse "^2.4.0"
|
||||
sonic-boom "^4.0.1"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
pino-std-serializers@^6.0.0:
|
||||
version "6.2.2"
|
||||
resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz"
|
||||
@@ -883,9 +1272,9 @@ pino-std-serializers@^7.0.0:
|
||||
resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz"
|
||||
integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==
|
||||
|
||||
pino@^8.15.0:
|
||||
pino@^8.21.0:
|
||||
version "8.21.0"
|
||||
resolved "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d"
|
||||
integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
@@ -917,11 +1306,33 @@ pino@^9.2.0:
|
||||
sonic-boom "^4.0.1"
|
||||
thread-stream "^3.0.0"
|
||||
|
||||
pixelmatch@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-4.0.2.tgz#8f47dcec5011b477b67db03c243bc1f3085e8854"
|
||||
integrity sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==
|
||||
dependencies:
|
||||
pngjs "^3.0.0"
|
||||
|
||||
pngjs@^3.0.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
|
||||
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
|
||||
|
||||
pngjs@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
|
||||
integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
|
||||
|
||||
postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
prettier@^3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5"
|
||||
integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==
|
||||
|
||||
process-warning@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz"
|
||||
@@ -932,6 +1343,15 @@ process@^0.11.10:
|
||||
resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
|
||||
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
|
||||
|
||||
prop-types@^15.6.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
proxy-addr@~2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz"
|
||||
@@ -940,6 +1360,14 @@ proxy-addr@~2.0.7:
|
||||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
qs@6.11.0:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz"
|
||||
@@ -967,6 +1395,21 @@ raw-body@2.5.2:
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-responsive@^10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-responsive/-/react-responsive-10.0.1.tgz#293d4d2562da93409861216f0110d146c5676eb3"
|
||||
integrity sha512-OM5/cRvbtUWEX8le8RCT8scA8y2OPtb0Q/IViEyCEM5FBN8lRrkUOZnu87I88A6njxDldvxG+rLBxWiA7/UM9g==
|
||||
dependencies:
|
||||
hyphenate-style-name "^1.0.0"
|
||||
matchmediaquery "^0.4.2"
|
||||
prop-types "^15.6.1"
|
||||
shallow-equal "^3.1.0"
|
||||
|
||||
react@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
|
||||
@@ -985,11 +1428,34 @@ readable-stream@^4.0.0:
|
||||
process "^0.11.10"
|
||||
string_decoder "^1.3.0"
|
||||
|
||||
readable-stream@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91"
|
||||
integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==
|
||||
dependencies:
|
||||
abort-controller "^3.0.0"
|
||||
buffer "^6.0.3"
|
||||
events "^3.3.0"
|
||||
process "^0.11.10"
|
||||
string_decoder "^1.3.0"
|
||||
|
||||
readable-web-to-node-stream@^3.0.0:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz#392ba37707af5bf62d725c36c1b5d6ef4119eefc"
|
||||
integrity sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==
|
||||
dependencies:
|
||||
readable-stream "^4.7.0"
|
||||
|
||||
real-require@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz"
|
||||
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
|
||||
|
||||
regenerator-runtime@^0.13.3:
|
||||
version "0.13.11"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
|
||||
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
||||
|
||||
roarr@^7.0.4:
|
||||
version "7.21.1"
|
||||
resolved "https://registry.npmjs.org/roarr/-/roarr-7.21.1.tgz"
|
||||
@@ -1030,6 +1496,11 @@ satori@^0.10.13:
|
||||
postcss-value-parser "^4.2.0"
|
||||
yoga-wasm-web "^0.3.3"
|
||||
|
||||
secure-json-parse@^2.4.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
|
||||
integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
|
||||
|
||||
semver-compare@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz"
|
||||
@@ -1081,6 +1552,11 @@ setprototypeof@1.2.0:
|
||||
resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
|
||||
shallow-equal@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-3.1.0.tgz#e7a54bac629c7f248eff6c2f5b63122ba4320bec"
|
||||
integrity sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==
|
||||
|
||||
side-channel@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz"
|
||||
@@ -1127,6 +1603,19 @@ string_decoder@^1.3.0:
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
strtok3@^6.2.4:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0"
|
||||
integrity sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==
|
||||
dependencies:
|
||||
"@tokenizer/token" "^0.3.0"
|
||||
peek-readable "^4.1.0"
|
||||
|
||||
thread-stream@^2.6.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz"
|
||||
@@ -1141,11 +1630,21 @@ thread-stream@^3.0.0:
|
||||
dependencies:
|
||||
real-require "^0.2.0"
|
||||
|
||||
timm@^1.6.1:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f"
|
||||
integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==
|
||||
|
||||
tiny-inflate@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz"
|
||||
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
|
||||
|
||||
tinycolor2@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
|
||||
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
|
||||
|
||||
tlds@^1.234.0:
|
||||
version "1.252.0"
|
||||
resolved "https://registry.npmjs.org/tlds/-/tlds-1.252.0.tgz"
|
||||
@@ -1156,6 +1655,19 @@ toidentifier@1.0.1:
|
||||
resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
|
||||
token-types@^4.1.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/token-types/-/token-types-4.2.1.tgz#0f897f03665846982806e138977dbe72d44df753"
|
||||
integrity sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==
|
||||
dependencies:
|
||||
"@tokenizer/token" "^0.3.0"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
ts-node@^10.9.2:
|
||||
version "10.9.2"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
|
||||
@@ -1238,6 +1750,13 @@ unpipe@1.0.0, unpipe@~1.0.0:
|
||||
resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||
|
||||
utif2@^4.0.1:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/utif2/-/utif2-4.1.0.tgz#e768d37bd619b995d56d9780b5d2b4611a3d932b"
|
||||
integrity sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==
|
||||
dependencies:
|
||||
pako "^1.0.11"
|
||||
|
||||
utils-merge@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
|
||||
@@ -1253,6 +1772,34 @@ vary@~1.1.2:
|
||||
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz"
|
||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
whatwg-fetch@^3.4.1:
|
||||
version "3.6.20"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70"
|
||||
integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==
|
||||
|
||||
whatwg-mimetype@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
|
||||
integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
@@ -1263,7 +1810,7 @@ yoga-wasm-web@^0.3.3:
|
||||
resolved "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz"
|
||||
integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==
|
||||
|
||||
zod@^3.21.4:
|
||||
zod@^3.23.8:
|
||||
version "3.23.8"
|
||||
resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz"
|
||||
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
||||
|
||||
Reference in New Issue
Block a user