diff --git a/.github/workflows/build-and-push-ogcard-aws.yaml b/.github/workflows/build-and-push-ogcard-aws.yaml new file mode 100644 index 0000000000..5d6ff041d3 --- /dev/null +++ b/.github/workflows/build-and-push-ogcard-aws.yaml @@ -0,0 +1,55 @@ +name: build-and-push-ogcard-aws +on: + push: + branches: + - divy/bskycard + +env: + REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} + USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} + PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }} + IMAGE_NAME: bskyogcard + +jobs: + ogcard-container-aws: + if: github.repository == 'bluesky-social/social-app' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v1 + + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.USERNAME}} + password: ${{ env.PASSWORD }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,enable=true,priority=100,prefix=,suffix=,format=long + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + file: ./Dockerfile.bskyogcard + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile.bskyogcard b/Dockerfile.bskyogcard new file mode 100644 index 0000000000..aa68add595 --- /dev/null +++ b/Dockerfile.bskyogcard @@ -0,0 +1,41 @@ +FROM node:20.11-alpine3.18 as build + +# Move files into the image and install +WORKDIR /app + +COPY ./bskyogcard/package.json ./ +COPY ./bskyogcard/yarn.lock ./ +RUN yarn install --frozen-lockfile + +COPY ./bskyogcard ./ + +# build then prune dev deps +RUN yarn build +RUN yarn install --production --ignore-scripts --prefer-offline + +# Uses assets from build stage to reduce build size +FROM node:20.11-alpine3.18 + +RUN apk add --update dumb-init + +# Avoid zombie processes, handle signal forwarding +ENTRYPOINT ["dumb-init", "--"] + +WORKDIR /app +COPY --from=build /app /app +RUN mkdir /app/data && chown node /app/data + +VOLUME /app/data +EXPOSE 3000 +ENV CARD_PORT=3000 +ENV NODE_ENV=production +# potential perf issues w/ io_uring on this version of node +ENV UV_USE_IO_URING=0 + +# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user +USER node +CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "dist/bin.js"] + +LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app +LABEL org.opencontainers.image.description="Bsky Card Service" +LABEL org.opencontainers.image.licenses=UNLICENSED diff --git a/bskyogcard/package.json b/bskyogcard/package.json new file mode 100644 index 0000000000..3be1337fc3 --- /dev/null +++ b/bskyogcard/package.json @@ -0,0 +1,24 @@ +{ + "name": "bskyogcard", + "version": "0.0.0", + "type": "module", + "main": "src/index.ts", + "scripts": { + "start": "node --loader ts-node/esm ./src/bin.ts", + "build": "tsc && cp -r src/assets dist/assets" + }, + "dependencies": { + "@atproto/api": "0.12.19-next.0", + "@atproto/common": "^0.4.0", + "@resvg/resvg-js": "^2.6.2", + "express": "^4.19.2", + "http-terminator": "^3.2.0", + "pino": "^9.2.0", + "react": "^18.3.1", + "satori": "^0.10.13" + }, + "devDependencies": { + "@types/node": "^20.14.3", + "typescript": "^5.4.5" + } +} diff --git a/bskyogcard/src/assets/Inter-Bold.ttf b/bskyogcard/src/assets/Inter-Bold.ttf new file mode 100644 index 0000000000..fe23eeb9c9 Binary files /dev/null and b/bskyogcard/src/assets/Inter-Bold.ttf differ diff --git a/bskyogcard/src/bin.ts b/bskyogcard/src/bin.ts new file mode 100644 index 0000000000..ff550809db --- /dev/null +++ b/bskyogcard/src/bin.ts @@ -0,0 +1,48 @@ +import cluster, {Worker} from 'node:cluster' + +import {envInt} from '@atproto/common' + +import {CardService, envToCfg, httpLogger, readEnv} from './index.js' + +async function main() { + const env = readEnv() + const cfg = envToCfg(env) + const card = await CardService.create(cfg) + await card.start() + httpLogger.info('card service is running') + process.on('SIGTERM', async () => { + httpLogger.info('card service is stopping') + await card.destroy() + httpLogger.info('card service is stopped') + if (cluster.isWorker) process.exit(0) + }) +} + +const workerCount = envInt('CARD_CLUSTER_WORKER_COUNT') + +if (workerCount) { + if (cluster.isPrimary) { + httpLogger.info(`primary ${process.pid} is running`) + const workers = new Set() + for (let i = 0; i < workerCount; ++i) { + workers.add(cluster.fork()) + } + let teardown = false + cluster.on('exit', worker => { + workers.delete(worker) + if (!teardown) { + workers.add(cluster.fork()) // restart on crash + } + }) + process.on('SIGTERM', () => { + teardown = true + httpLogger.info('disconnecting workers') + workers.forEach(w => w.kill('SIGTERM')) + }) + } else { + httpLogger.info(`worker ${process.pid} is running`) + main() + } +} else { + main() // non-clustering +} diff --git a/bskyogcard/src/components/Butterfly.tsx b/bskyogcard/src/components/Butterfly.tsx new file mode 100644 index 0000000000..5a4124975c --- /dev/null +++ b/bskyogcard/src/components/Butterfly.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +export function Butterfly(props: React.SVGAttributes) { + return ( + + + + ) +} diff --git a/bskyogcard/src/components/Img.tsx b/bskyogcard/src/components/Img.tsx new file mode 100644 index 0000000000..dac223180c --- /dev/null +++ b/bskyogcard/src/components/Img.tsx @@ -0,0 +1,10 @@ +import React from 'react' + +export function Img( + props: Omit, 'src'> & {src: Buffer}, +) { + const {src, ...others} = props + return ( + + ) +} diff --git a/bskyogcard/src/components/StarterPack.tsx b/bskyogcard/src/components/StarterPack.tsx new file mode 100644 index 0000000000..f73442190c --- /dev/null +++ b/bskyogcard/src/components/StarterPack.tsx @@ -0,0 +1,149 @@ +/* eslint-disable bsky-internal/avoid-unwrapped-text */ +import React from 'react' +import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' + +import {Butterfly} from './Butterfly.js' +import {Img} from './Img.js' + +export const STARTERPACK_HEIGHT = 630 +export const STARTERPACK_WIDTH = 1200 +export const TILE_SIZE = STARTERPACK_HEIGHT / 3 + +const GRADIENT_TOP = '#0A7AFF' +const GRADIENT_BOTTOM = '#59B9FF' +const IMAGE_STROKE = '#359CFF' + +export function StarterPack(props: { + starterPack: AppBskyGraphDefs.StarterPackView + images: Map +}) { + const {starterPack, images} = props + const record = AppBskyGraphStarterpack.isRecord(starterPack.record) + ? starterPack.record + : null + const imagesArray = [...images.values()] + const imageOfCreator = images.get(starterPack.creator.did) + const imagesExceptCreator = [...images.entries()] + .filter(([did]) => did !== starterPack.creator.did) + .map(([, image]) => image) + const imagesAcross: Buffer[] = [] + if (imageOfCreator) { + if (imagesExceptCreator.length >= 6) { + imagesAcross.push(...imagesExceptCreator.slice(0, 3)) + imagesAcross.push(imageOfCreator) + imagesAcross.push(...imagesExceptCreator.slice(3, 6)) + } else { + const firstHalf = Math.floor(imagesExceptCreator.length / 2) + imagesAcross.push(...imagesExceptCreator.slice(0, firstHalf)) + imagesAcross.push(imageOfCreator) + imagesAcross.push( + ...imagesExceptCreator.slice(firstHalf, imagesExceptCreator.length), + ) + } + } else { + imagesAcross.push(...imagesExceptCreator.slice(0, 7)) + } + return ( +
+ {/* image tiles */} +
+ {[...Array(18)].map((_, i) => { + const image = imagesArray.at(i % imagesArray.length) + return ( +
+ {image && } +
+ ) + })} + {/* background overlay */} +
+
+ {/* foreground text & images */} +
+
+ JOIN THE CONVERSATION +
+
+ {imagesAcross.map((image, i) => { + return ( +
+ +
+ ) + })} +
+
+ {record?.name || 'Starter Pack'} +
+
+ on Bluesky +
+
+
+ ) +} diff --git a/bskyogcard/src/config.ts b/bskyogcard/src/config.ts new file mode 100644 index 0000000000..fafa18e743 --- /dev/null +++ b/bskyogcard/src/config.ts @@ -0,0 +1,40 @@ +import {envInt, envStr} from '@atproto/common' + +export type Config = { + service: ServiceConfig +} + +export type ServiceConfig = { + port: number + version?: string + appviewUrl: string + originVerify?: string +} + +export type Environment = { + port?: number + version?: string + appviewUrl?: string + originVerify?: string +} + +export const readEnv = (): Environment => { + return { + port: envInt('CARD_PORT'), + version: envStr('CARD_VERSION'), + appviewUrl: envStr('CARD_APPVIEW_URL'), + originVerify: envStr('CARD_ORIGIN_VERIFY'), + } +} + +export const envToCfg = (env: Environment): Config => { + const serviceCfg: ServiceConfig = { + port: env.port ?? 3000, + version: env.version, + appviewUrl: env.appviewUrl ?? 'https://api.bsky.app', + originVerify: env.originVerify, + } + return { + service: serviceCfg, + } +} diff --git a/bskyogcard/src/context.ts b/bskyogcard/src/context.ts new file mode 100644 index 0000000000..f92651cafb --- /dev/null +++ b/bskyogcard/src/context.ts @@ -0,0 +1,44 @@ +import {readFileSync} from 'node:fs' + +import {AtpAgent} from '@atproto/api' +import * as path from 'path' +import {fileURLToPath} from 'url' + +import {Config} from './config.js' + +const __DIRNAME = path.dirname(fileURLToPath(import.meta.url)) + +export type AppContextOptions = { + cfg: Config + appviewAgent: AtpAgent + fonts: {name: string; data: Buffer}[] +} + +export class AppContext { + cfg: Config + appviewAgent: AtpAgent + fonts: {name: string; data: Buffer}[] + abortController = new AbortController() + + constructor(private opts: AppContextOptions) { + this.cfg = this.opts.cfg + this.appviewAgent = this.opts.appviewAgent + this.fonts = this.opts.fonts + } + + static async fromConfig(cfg: Config, overrides?: Partial) { + const appviewAgent = new AtpAgent({service: cfg.service.appviewUrl}) + const fonts = [ + { + name: 'Inter', + data: readFileSync(path.join(__DIRNAME, 'assets', 'Inter-Bold.ttf')), + }, + ] + return new AppContext({ + cfg, + appviewAgent, + fonts, + ...overrides, + }) + } +} diff --git a/bskyogcard/src/index.ts b/bskyogcard/src/index.ts new file mode 100644 index 0000000000..ef8d48494d --- /dev/null +++ b/bskyogcard/src/index.ts @@ -0,0 +1,41 @@ +import events from 'node:events' +import http from 'node:http' + +import express from 'express' +import {createHttpTerminator, HttpTerminator} from 'http-terminator' + +import {Config} from './config.js' +import {AppContext} from './context.js' +import {default as routes, errorHandler} from './routes/index.js' + +export * from './config.js' +export * from './logger.js' + +export class CardService { + public server?: http.Server + private terminator?: HttpTerminator + + constructor(public app: express.Application, public ctx: AppContext) {} + + static async create(cfg: Config): Promise { + let app = express() + + const ctx = await AppContext.fromConfig(cfg) + app = routes(ctx, app) + app.use(errorHandler) + + return new CardService(app, ctx) + } + + async start() { + this.server = this.app.listen(this.ctx.cfg.service.port) + this.server.keepAliveTimeout = 90000 + this.terminator = createHttpTerminator({server: this.server}) + await events.once(this.server, 'listening') + } + + async destroy() { + this.ctx.abortController.abort() + await this.terminator?.terminate() + } +} diff --git a/bskyogcard/src/logger.ts b/bskyogcard/src/logger.ts new file mode 100644 index 0000000000..04b5d90469 --- /dev/null +++ b/bskyogcard/src/logger.ts @@ -0,0 +1,3 @@ +import {subsystemLogger} from '@atproto/common' + +export const httpLogger = subsystemLogger('bskyogcard') diff --git a/bskyogcard/src/routes/health.ts b/bskyogcard/src/routes/health.ts new file mode 100644 index 0000000000..0cc69515eb --- /dev/null +++ b/bskyogcard/src/routes/health.ts @@ -0,0 +1,14 @@ +import {Express} from 'express' + +import {AppContext} from '../context.js' +import {handler} from './util.js' + +export default function (ctx: AppContext, app: Express) { + return app.get( + '/_health', + handler(async (_req, res) => { + const {version} = ctx.cfg.service + return res.send({version}) + }), + ) +} diff --git a/bskyogcard/src/routes/index.ts b/bskyogcard/src/routes/index.ts new file mode 100644 index 0000000000..0c40f89d3b --- /dev/null +++ b/bskyogcard/src/routes/index.ts @@ -0,0 +1,13 @@ +import {Express} from 'express' + +import {AppContext} from '../context.js' +import {default as health} from './health.js' +import {default as starterPack} from './starter-pack.js' + +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 + return app +} diff --git a/bskyogcard/src/routes/starter-pack.tsx b/bskyogcard/src/routes/starter-pack.tsx new file mode 100644 index 0000000000..cb3a553272 --- /dev/null +++ b/bskyogcard/src/routes/starter-pack.tsx @@ -0,0 +1,102 @@ +import assert from 'node:assert' + +import React from 'react' +import {AppBskyGraphDefs, AtUri} from '@atproto/api' +import resvg from '@resvg/resvg-js' +import {Express} from 'express' +import satori from 'satori' + +import { + StarterPack, + STARTERPACK_HEIGHT, + STARTERPACK_WIDTH, +} from '../components/StarterPack.js' +import {AppContext} from '../context.js' +import {httpLogger} from '../logger.js' +import {handler, originVerifyMiddleware} from './util.js' + +export default function (ctx: AppContext, app: Express) { + return app.get( + '/start/:actor/:rkey', + originVerifyMiddleware(ctx), + handler(async (req, res) => { + const {actor, rkey} = req.params + const uri = AtUri.make(actor, 'app.bsky.graph.starterpack', rkey) + let starterPack: AppBskyGraphDefs.StarterPackView + try { + const result = await ctx.appviewAgent.api.app.bsky.graph.getStarterPack( + {starterPack: uri.toString()}, + ) + starterPack = result.data.starterPack + } catch (err) { + httpLogger.warn( + {err, uri: uri.toString()}, + 'could not fetch starter pack', + ) + return res.status(404).end('not found') + } + const imageEntries = await Promise.all( + [starterPack.creator] + .concat(starterPack.listItemsSample.map(li => li.subject)) + // has avatar + .filter(p => p.avatar) + // no sensitive labels + .filter(p => !p.labels.some(l => hideAvatarLabels.has(l.val))) + .map(async p => { + try { + assert(p.avatar) + const image = await getImage(p.avatar) + return [p.did, image] as const + } catch (err) { + httpLogger.warn( + {err, uri: uri.toString(), did: p.did}, + 'could not fetch image', + ) + return [p.did, null] as const + } + }), + ) + const images = new Map( + imageEntries.filter(([_, image]) => image !== null).slice(0, 7), + ) + const svg = await satori( + , + { + fonts: ctx.fonts, + height: STARTERPACK_HEIGHT, + width: STARTERPACK_WIDTH, + }, + ) + const output = await resvg.renderAsync(svg) + res.statusCode = 200 + res.setHeader('content-type', 'image/png') + res.setHeader('cdn-tag', [...images.keys()].join(',')) + return res.end(output.asPng()) + }), + ) +} + +async function getImage(url: string) { + const response = await fetch(url) + const arrayBuf = await response.arrayBuffer() // must drain body even if it will be discarded + if (response.status !== 200) return null + return Buffer.from(arrayBuf) +} + +const hideAvatarLabels = new Set([ + '!hide', + '!warn', + 'porn', + 'sexual', + 'nudity', + 'sexual-figurative', + 'graphic-media', + 'self-harm', + 'sensitive', + 'security', + 'impersonation', + 'scam', + 'spam', + 'misleading', + 'inauthentic', +]) diff --git a/bskyogcard/src/routes/util.ts b/bskyogcard/src/routes/util.ts new file mode 100644 index 0000000000..718ed592a1 --- /dev/null +++ b/bskyogcard/src/routes/util.ts @@ -0,0 +1,36 @@ +import {ErrorRequestHandler, Request, RequestHandler, Response} from 'express' + +import {AppContext} from '../context.js' +import {httpLogger} from '../logger.js' + +export type Handler = (req: Request, res: Response) => Awaited + +export const handler = (runHandler: Handler): RequestHandler => { + return async (req, res, next) => { + try { + await runHandler(req, res) + } catch (err) { + next(err) + } + } +} + +export function originVerifyMiddleware(ctx: AppContext): RequestHandler { + const {originVerify} = ctx.cfg.service + if (!originVerify) return (_req, _res, next) => next() + return (req, res, next) => { + const verifyHeader = req.headers['x-origin-verify'] + if (verifyHeader !== originVerify) { + return res.status(404).end('not found') + } + next() + } +} + +export const errorHandler: ErrorRequestHandler = (err, req, res, next) => { + httpLogger.error({err}, 'request error') + if (res.headersSent) { + return next(err) + } + return res.status(500).end('server error') +} diff --git a/bskyogcard/tsconfig.json b/bskyogcard/tsconfig.json new file mode 100644 index 0000000000..a5c3beecb1 --- /dev/null +++ b/bskyogcard/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "esModuleInterop": true, + "moduleResolution": "NodeNext", + "jsx": "react-jsx", + "outDir": "dist" + }, + "include": ["./src/index.ts", "./src/bin.ts"] +} diff --git a/bskyogcard/yarn.lock b/bskyogcard/yarn.lock new file mode 100644 index 0000000000..0403efb84e --- /dev/null +++ b/bskyogcard/yarn.lock @@ -0,0 +1,1113 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@atproto/api@0.12.19-next.0": + version "0.12.19-next.0" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.19-next.0.tgz#9592476cbdba8482d0fd8d65e20275c95d6d5fd4" + integrity sha512-wyWr4uIabTgDTBY99y3QyrFxcIx1Mh4DkURgSv8sd/b+w0lfrZAJh0Gg9BXdg/iIjcf/M2lCTL04r0vASfkMVg== + dependencies: + "@atproto/common-web" "^0.3.0" + "@atproto/lexicon" "^0.4.0" + "@atproto/syntax" "^0.3.0" + "@atproto/xrpc" "^0.5.0" + multiformats "^9.9.0" + tlds "^1.234.0" + +"@atproto/common-web@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.0.tgz#36da8c2c31d8cf8a140c3c8f03223319bf4430bb" + integrity sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA== + dependencies: + graphemer "^1.4.0" + multiformats "^9.9.0" + uint8arrays "3.0.0" + zod "^3.21.4" + +"@atproto/common@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.0.tgz#d77696c7eb545426df727837d9ee333b429fe7ef" + integrity sha512-yOXuPlCjT/OK9j+neIGYn9wkxx/AlxQSucysAF0xgwu0Ji8jAtKBf9Jv6R5ObYAjAD/kVUvEYumle+Yq/R9/7g== + dependencies: + "@atproto/common-web" "^0.3.0" + "@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" + +"@atproto/lexicon@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.0.tgz#63e8829945d80c25524882caa8ed27b1151cc576" + integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ== + dependencies: + "@atproto/common-web" "^0.3.0" + "@atproto/syntax" "^0.3.0" + iso-datestring-validator "^2.2.2" + multiformats "^9.9.0" + zod "^3.21.4" + +"@atproto/syntax@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3" + integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA== + +"@atproto/xrpc@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.5.0.tgz#dacbfd8f7b13f0ab5bd56f8fdd4b460e132a6032" + integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog== + dependencies: + "@atproto/lexicon" "^0.4.0" + zod "^3.21.4" + +"@cbor-extract/cbor-extract-darwin-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz#8d65cb861a99622e1b4a268e2d522d2ec6137338" + integrity sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w== + +"@cbor-extract/cbor-extract-darwin-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz#9fbec199c888c5ec485a1839f4fad0485ab6c40a" + integrity sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w== + +"@cbor-extract/cbor-extract-linux-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz#bf77e0db4a1d2200a5aa072e02210d5043e953ae" + integrity sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ== + +"@cbor-extract/cbor-extract-linux-arm@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz#491335037eb8533ed8e21b139c59f6df04e39709" + integrity sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q== + +"@cbor-extract/cbor-extract-linux-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz#672574485ccd24759bf8fb8eab9dbca517d35b97" + integrity sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw== + +"@cbor-extract/cbor-extract-win32-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz#4b3f07af047f984c082de34b116e765cb9af975f" + integrity sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w== + +"@ipld/dag-cbor@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz#aa31b28afb11a807c3d627828a344e5521ac4a1e" + integrity sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA== + dependencies: + cborg "^1.6.0" + multiformats "^9.5.4" + +"@resvg/resvg-js-android-arm-eabi@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz#e761e0b688127db64879f455178c92468a9aeabe" + integrity sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA== + +"@resvg/resvg-js-android-arm64@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz#b8cb564d7f6b3f37d9b43129f5dc5fe171e249e4" + integrity sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ== + +"@resvg/resvg-js-darwin-arm64@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz#49bd3faeda5c49f53302d970e6e79d006de18e7d" + integrity sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A== + +"@resvg/resvg-js-darwin-x64@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz#e1344173aa27bfb4d880ab576d1acf1c1648faca" + integrity sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw== + +"@resvg/resvg-js-linux-arm-gnueabihf@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz#34c445eba45efd68f6130b2ab426d76a7424253d" + integrity sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw== + +"@resvg/resvg-js-linux-arm64-gnu@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz#30da47087dd8153182198b94fe9f8d994890dae5" + integrity sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg== + +"@resvg/resvg-js-linux-arm64-musl@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz#5d75b8ff5c83103729c1ca3779987302753c50d4" + integrity sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg== + +"@resvg/resvg-js-linux-x64-gnu@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz#411abedfaee5edc57cbb7701736cecba522e26f3" + integrity sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw== + +"@resvg/resvg-js-linux-x64-musl@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz#fe4984038f0372f279e3ff570b72934dd7eb2a5c" + integrity sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ== + +"@resvg/resvg-js-win32-arm64-msvc@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz#d3a053cf7ff687087a2106330c0fdaae706254d1" + integrity sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ== + +"@resvg/resvg-js-win32-ia32-msvc@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz#7cdda1ce29ef7209e28191d917fa5bef0624a4ad" + integrity sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w== + +"@resvg/resvg-js-win32-x64-msvc@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz#cb0ad04525d65f3def4c8d346157a57976d5b388" + integrity sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ== + +"@resvg/resvg-js@^2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@resvg/resvg-js/-/resvg-js-2.6.2.tgz#3e92a907d88d879256c585347c5b21a7f3bb5b46" + integrity sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q== + optionalDependencies: + "@resvg/resvg-js-android-arm-eabi" "2.6.2" + "@resvg/resvg-js-android-arm64" "2.6.2" + "@resvg/resvg-js-darwin-arm64" "2.6.2" + "@resvg/resvg-js-darwin-x64" "2.6.2" + "@resvg/resvg-js-linux-arm-gnueabihf" "2.6.2" + "@resvg/resvg-js-linux-arm64-gnu" "2.6.2" + "@resvg/resvg-js-linux-arm64-musl" "2.6.2" + "@resvg/resvg-js-linux-x64-gnu" "2.6.2" + "@resvg/resvg-js-linux-x64-musl" "2.6.2" + "@resvg/resvg-js-win32-arm64-msvc" "2.6.2" + "@resvg/resvg-js-win32-ia32-msvc" "2.6.2" + "@resvg/resvg-js-win32-x64-msvc" "2.6.2" + +"@shuding/opentype.js@1.4.0-beta.0": + version "1.4.0-beta.0" + resolved "https://registry.yarnpkg.com/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz#5d1e7e9e056f546aad41df1c5043f8f85d39e24b" + integrity sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA== + dependencies: + fflate "^0.7.3" + string.prototype.codepointat "^0.2.1" + +"@types/node@^20.14.3": + version "20.14.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.3.tgz#7a9a5d009b0861e7f337166dc435dbfd758db92d" + integrity sha512-Nuzqa6WAxeGnve6SXqiPAM9rA++VQs+iLZ1DDd56y0gdvygSZlQvZuvdFPR3yLqkVxPu4WrO02iDEyH1g+wazw== + dependencies: + undici-types "~5.26.4" + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + +base64-js@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" + integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +boolean@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +camelize@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" + integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== + +cbor-extract@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.2.0.tgz#cee78e630cbeae3918d1e2e58e0cebaf3a3be840" + integrity sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA== + dependencies: + node-gyp-build-optional-packages "5.1.1" + optionalDependencies: + "@cbor-extract/cbor-extract-darwin-arm64" "2.2.0" + "@cbor-extract/cbor-extract-darwin-x64" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm64" "2.2.0" + "@cbor-extract/cbor-extract-linux-x64" "2.2.0" + "@cbor-extract/cbor-extract-win32-x64" "2.2.0" + +cbor-x@^1.5.1: + version "1.5.9" + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.9.tgz#ed6b2afcd7884bdd697674bfb7332c1473a13ecf" + integrity sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ== + optionalDependencies: + cbor-extract "^2.2.0" + +cborg@^1.6.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1" + integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug== + +color-name@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +css-background-parser@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/css-background-parser/-/css-background-parser-0.1.0.tgz#48a17f7fe6d4d4f1bca3177ddf16c5617950741b" + integrity sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA== + +css-box-shadow@1.0.0-3: + version "1.0.0-3" + resolved "https://registry.yarnpkg.com/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz#9eaeb7140947bf5d649fc49a19e4bbaa5f602713" + integrity sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg== + +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== + +css-to-react-native@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" + integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^10.2.1: + version "10.3.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" + integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escape-html@^1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-printf@^1.6.9: + version "1.6.9" + resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676" + integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg== + dependencies: + boolean "^3.1.4" + +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + +fflate@^0.7.3: + version "0.7.4" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.7.4.tgz#61587e5d958fdabb5a9368a302c25363f4f69f50" + integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw== + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hex-rgb@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776" + integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-terminator@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/http-terminator/-/http-terminator-3.2.0.tgz#bc158d2694b733ca4fbf22a35065a81a609fb3e9" + integrity sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g== + dependencies: + delay "^5.0.0" + p-wait-for "^3.2.0" + roarr "^7.0.4" + type-fest "^2.3.3" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +iso-datestring-validator@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz#2daa80d2900b7a954f9f731d42f96ee0c19a6895" + integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +linebreak@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/linebreak/-/linebreak-1.1.0.tgz#831cf378d98bced381d8ab118f852bd50d81e46b" + integrity sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ== + dependencies: + base64-js "0.0.8" + unicode-trie "^2.0.0" + +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.9.0: + version "9.9.0" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" + integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-gyp-build-optional-packages@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c" + integrity sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw== + dependencies: + detect-libc "^2.0.1" + +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + +p-timeout@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + +p-wait-for@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-3.2.0.tgz#640429bcabf3b0dd9f492c31539c5718cb6a3f1f" + integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA== + dependencies: + p-timeout "^3.0.0" + +pako@^0.2.5: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== + +parse-css-color@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/parse-css-color/-/parse-css-color-0.2.1.tgz#b687a583f2e42e66ffdfce80a570706966e807c9" + integrity sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg== + dependencies: + color-name "^1.1.4" + hex-rgb "^4.1.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +pino-abstract-transport@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5" + integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-std-serializers@^6.0.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz#d9a9b5f2b9a402486a5fc4db0a737570a860aab3" + integrity sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA== + +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@^8.15.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.21.0.tgz#e1207f3675a2722940d62da79a7a55a98409f00d" + integrity sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^6.0.0" + process-warning "^3.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^3.7.0" + thread-stream "^2.6.0" + +pino@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.2.0.tgz#e77a9516f3a3e5550d9b76d9f65ac6118ef02bdd" + integrity sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^1.2.0" + pino-std-serializers "^7.0.0" + process-warning "^3.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + +postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +process-warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b" + integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +readable-stream@^4.0.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + +roarr@^7.0.4: + version "7.21.1" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.21.1.tgz#fd6452ca822a65f736c35e5372f04ee9f2ca3851" + integrity sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ== + dependencies: + fast-printf "^1.6.9" + safe-stable-stringify "^2.4.3" + semver-compare "^1.0.0" + +safe-buffer@5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-stable-stringify@^2.3.1, safe-stable-stringify@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +satori@^0.10.13: + version "0.10.13" + resolved "https://registry.yarnpkg.com/satori/-/satori-0.10.13.tgz#658a9920f55268d2002819387a80a0b6d4bdc262" + integrity sha512-klCwkVYMQ/ZN5inJLHzrUmGwoRfsdP7idB5hfpJ1jfiJk1ErDitK8Hkc6Kll1+Ox2WtqEuGecSZLnmup3CGzvQ== + dependencies: + "@shuding/opentype.js" "1.4.0-beta.0" + css-background-parser "^0.1.0" + css-box-shadow "1.0.0-3" + css-to-react-native "^3.0.0" + emoji-regex "^10.2.1" + escape-html "^1.0.3" + linebreak "^1.1.0" + parse-css-color "^0.2.1" + postcss-value-parser "^4.2.0" + yoga-wasm-web "^0.3.3" + +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +sonic-boom@^3.7.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.8.1.tgz#d5ba8c4e26d6176c9a1d14d549d9ff579a163422" + integrity sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg== + dependencies: + atomic-sleep "^1.0.0" + +sonic-boom@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30" + integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ== + dependencies: + atomic-sleep "^1.0.0" + +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +string.prototype.codepointat@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" + integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== + +string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +thread-stream@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.7.0.tgz#d8a8e1b3fd538a6cca8ce69dbe5d3d097b601e11" + integrity sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw== + dependencies: + real-require "^0.2.0" + +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + +tiny-inflate@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" + integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== + +tlds@^1.234.0: + version "1.252.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.252.0.tgz#71d9617f4ef4cc7347843bee72428e71b8b0f419" + integrity sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ== + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-fest@^2.3.3: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^5.4.5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +uint8arrays@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" + integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA== + dependencies: + multiformats "^9.4.2" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unicode-trie@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-2.0.0.tgz#8fd8845696e2e14a8b67d78fa9e0dd2cad62fec8" + integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ== + dependencies: + pako "^0.2.5" + tiny-inflate "^1.0.0" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +yoga-wasm-web@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz#eb8e9fcb18e5e651994732f19a220cb885d932ba" + integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA== + +zod@^3.21.4: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== diff --git a/package.json b/package.json index 4178369035..bcd5a1d37e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" }, "dependencies": { - "@atproto/api": "^0.12.19", + "@atproto/api": "^0.12.20", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", @@ -58,8 +58,9 @@ "@expo/webpack-config": "^19.0.0", "@floating-ui/dom": "^1.6.3", "@floating-ui/react-dom": "^2.0.8", - "@formatjs/intl-locale": "^3.4.3", - "@formatjs/intl-pluralrules": "^5.2.10", + "@formatjs/intl-locale": "^4.0.0", + "@formatjs/intl-numberformat": "^8.10.3", + "@formatjs/intl-pluralrules": "^5.2.14", "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", diff --git a/src/App.native.tsx b/src/App.native.tsx index 18461fdd05..4c73d87525 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -24,6 +24,7 @@ import { import {s} from '#/lib/styles' import {ThemeProvider} from '#/lib/ThemeContext' import {logger} from '#/logger' +import {Provider as A11yProvider} from '#/state/a11y' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as InvitesStateProvider} from '#/state/invites' @@ -152,27 +153,29 @@ function App() { * that is set up in the InnerApp component above. */ return ( - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/App.web.tsx b/src/App.web.tsx index 6af3c7d6fb..00939c9eb4 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -13,6 +13,7 @@ import {QueryProvider} from '#/lib/react-query' import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {ThemeProvider} from '#/lib/ThemeContext' import {logger} from '#/logger' +import {Provider as A11yProvider} from '#/state/a11y' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as InvitesStateProvider} from '#/state/invites' @@ -135,25 +136,27 @@ function App() { * that is set up in the InnerApp component above. */ return ( - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 67b89e2627..5d4ba0e3f7 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -54,8 +54,8 @@ import {useModalControls} from './state/modals' import {useUnreadNotifications} from './state/queries/notifications/unread' import {useSession} from './state/session' import { - setEmailConfirmationRequested, shouldRequestEmailConfirmation, + snoozeEmailConfirmationPrompt, } from './state/shell/reminders' import {AccessibilitySettingsScreen} from './view/screens/AccessibilitySettings' import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines' @@ -585,7 +585,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) { openModal({name: 'verify-email', showReminder: true}) - setEmailConfirmationRequested() + snoozeEmailConfirmationPrompt() } } diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 982f422134..54d9eaf3b0 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -88,336 +88,355 @@ export function useButtonContext() { return React.useContext(Context) } -export function Button({ - children, - variant, - color, - size, - shape = 'default', - label, - disabled = false, - style, - hoverStyle: hoverStyleProp, - ...rest -}: ButtonProps) { - const t = useTheme() - const [state, setState] = React.useState({ - pressed: false, - hovered: false, - focused: false, - }) - - const onPressIn = React.useCallback(() => { - setState(s => ({ - ...s, - pressed: true, - })) - }, [setState]) - const onPressOut = React.useCallback(() => { - setState(s => ({ - ...s, +export const Button = React.forwardRef( + ( + { + children, + variant, + color, + size, + shape = 'default', + label, + disabled = false, + style, + hoverStyle: hoverStyleProp, + ...rest + }, + ref, + ) => { + const t = useTheme() + const [state, setState] = React.useState({ pressed: false, - })) - }, [setState]) - const onHoverIn = React.useCallback(() => { - setState(s => ({ - ...s, - hovered: true, - })) - }, [setState]) - const onHoverOut = React.useCallback(() => { - setState(s => ({ - ...s, hovered: false, - })) - }, [setState]) - const onFocus = React.useCallback(() => { - setState(s => ({ - ...s, - focused: true, - })) - }, [setState]) - const onBlur = React.useCallback(() => { - setState(s => ({ - ...s, focused: false, - })) - }, [setState]) - - const {baseStyles, hoverStyles} = React.useMemo(() => { - const baseStyles: ViewStyle[] = [] - const hoverStyles: ViewStyle[] = [] - const light = t.name === 'light' - - if (color === 'primary') { - if (variant === 'solid') { - if (!disabled) { - baseStyles.push({ - backgroundColor: t.palette.primary_500, + }) + + const onPressIn = React.useCallback(() => { + setState(s => ({ + ...s, + pressed: true, + })) + }, [setState]) + const onPressOut = React.useCallback(() => { + setState(s => ({ + ...s, + pressed: false, + })) + }, [setState]) + const onHoverIn = React.useCallback(() => { + setState(s => ({ + ...s, + hovered: true, + })) + }, [setState]) + const onHoverOut = React.useCallback(() => { + setState(s => ({ + ...s, + hovered: false, + })) + }, [setState]) + const onFocus = React.useCallback(() => { + setState(s => ({ + ...s, + focused: true, + })) + }, [setState]) + const onBlur = React.useCallback(() => { + setState(s => ({ + ...s, + focused: false, + })) + }, [setState]) + + const {baseStyles, hoverStyles} = React.useMemo(() => { + const baseStyles: ViewStyle[] = [] + const hoverStyles: ViewStyle[] = [] + const light = t.name === 'light' + + if (color === 'primary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.primary_500, + }) + hoverStyles.push({ + backgroundColor: t.palette.primary_600, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.primary_700, + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, }) - hoverStyles.push({ - backgroundColor: t.palette.primary_600, - }) - } else { - baseStyles.push({ - backgroundColor: t.palette.primary_700, - }) - } - } else if (variant === 'outline') { - baseStyles.push(a.border, t.atoms.bg, { - borderWidth: 1, - }) - if (!disabled) { - baseStyles.push(a.border, { - borderColor: t.palette.primary_500, - }) - hoverStyles.push(a.border, { - backgroundColor: light - ? t.palette.primary_50 - : t.palette.primary_950, - }) - } else { - baseStyles.push(a.border, { - borderColor: light ? t.palette.primary_200 : t.palette.primary_900, - }) + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.primary_500, + }) + hoverStyles.push(a.border, { + backgroundColor: light + ? t.palette.primary_50 + : t.palette.primary_950, + }) + } else { + baseStyles.push(a.border, { + borderColor: light + ? t.palette.primary_200 + : t.palette.primary_900, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: light + ? t.palette.primary_100 + : t.palette.primary_900, + }) + } } - } else if (variant === 'ghost') { - if (!disabled) { - baseStyles.push(t.atoms.bg) - hoverStyles.push({ - backgroundColor: light - ? t.palette.primary_100 - : t.palette.primary_900, - }) - } - } - } else if (color === 'secondary') { - if (variant === 'solid') { - if (!disabled) { - baseStyles.push({ - backgroundColor: t.palette.contrast_25, - }) - hoverStyles.push({ - backgroundColor: t.palette.contrast_50, - }) - } else { - baseStyles.push({ - backgroundColor: t.palette.contrast_100, + } else if (color === 'secondary') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.contrast_25, + }) + hoverStyles.push({ + backgroundColor: t.palette.contrast_50, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.contrast_100, + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, }) - } - } else if (variant === 'outline') { - baseStyles.push(a.border, t.atoms.bg, { - borderWidth: 1, - }) - if (!disabled) { - baseStyles.push(a.border, { - borderColor: t.palette.contrast_300, - }) - hoverStyles.push(t.atoms.bg_contrast_50) - } else { - baseStyles.push(a.border, { - borderColor: t.palette.contrast_200, - }) + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_300, + }) + hoverStyles.push(t.atoms.bg_contrast_50) + } else { + baseStyles.push(a.border, { + borderColor: t.palette.contrast_200, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: t.palette.contrast_25, + }) + } } - } else if (variant === 'ghost') { - if (!disabled) { - baseStyles.push(t.atoms.bg) - hoverStyles.push({ - backgroundColor: t.palette.contrast_100, + } else if (color === 'negative') { + if (variant === 'solid') { + if (!disabled) { + baseStyles.push({ + backgroundColor: t.palette.negative_500, + }) + hoverStyles.push({ + backgroundColor: t.palette.negative_600, + }) + } else { + baseStyles.push({ + backgroundColor: t.palette.negative_700, + }) + } + } else if (variant === 'outline') { + baseStyles.push(a.border, t.atoms.bg, { + borderWidth: 1, }) + + if (!disabled) { + baseStyles.push(a.border, { + borderColor: t.palette.negative_500, + }) + hoverStyles.push(a.border, { + backgroundColor: light + ? t.palette.negative_50 + : t.palette.negative_975, + }) + } else { + baseStyles.push(a.border, { + borderColor: light + ? t.palette.negative_200 + : t.palette.negative_900, + }) + } + } else if (variant === 'ghost') { + if (!disabled) { + baseStyles.push(t.atoms.bg) + hoverStyles.push({ + backgroundColor: light + ? t.palette.negative_100 + : t.palette.negative_975, + }) + } } } - } else if (color === 'negative') { - if (variant === 'solid') { - if (!disabled) { - baseStyles.push({ - backgroundColor: t.palette.negative_500, - }) - hoverStyles.push({ - backgroundColor: t.palette.negative_600, - }) - } else { - baseStyles.push({ - backgroundColor: t.palette.negative_700, - }) - } - } else if (variant === 'outline') { - baseStyles.push(a.border, t.atoms.bg, { - borderWidth: 1, - }) - if (!disabled) { - baseStyles.push(a.border, { - borderColor: t.palette.negative_500, - }) - hoverStyles.push(a.border, { - backgroundColor: light - ? t.palette.negative_50 - : t.palette.negative_975, - }) - } else { - baseStyles.push(a.border, { - borderColor: light - ? t.palette.negative_200 - : t.palette.negative_900, - }) + if (shape === 'default') { + if (size === 'large') { + baseStyles.push( + {paddingVertical: 15}, + a.px_2xl, + a.rounded_sm, + a.gap_md, + ) + } else if (size === 'medium') { + baseStyles.push( + {paddingVertical: 12}, + a.px_2xl, + a.rounded_sm, + a.gap_md, + ) + } else if (size === 'small') { + baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) + } else if (size === 'xsmall') { + baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm) + } else if (size === 'tiny') { + baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs) } - } else if (variant === 'ghost') { - if (!disabled) { - baseStyles.push(t.atoms.bg) - hoverStyles.push({ - backgroundColor: light - ? t.palette.negative_100 - : t.palette.negative_975, - }) + } else if (shape === 'round' || shape === 'square') { + if (size === 'large') { + if (shape === 'round') { + baseStyles.push({height: 54, width: 54}) + } else { + baseStyles.push({height: 50, width: 50}) + } + } else if (size === 'small') { + baseStyles.push({height: 34, width: 34}) + } else if (size === 'xsmall') { + baseStyles.push({height: 28, width: 28}) + } else if (size === 'tiny') { + baseStyles.push({height: 20, width: 20}) } - } - } - if (shape === 'default') { - if (size === 'large') { - baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) - } else if (size === 'medium') { - baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md) - } else if (size === 'small') { - baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) - } else if (size === 'xsmall') { - baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm) - } else if (size === 'tiny') { - baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs) - } - } else if (shape === 'round' || shape === 'square') { - if (size === 'large') { if (shape === 'round') { - baseStyles.push({height: 54, width: 54}) - } else { - baseStyles.push({height: 50, width: 50}) - } - } else if (size === 'small') { - baseStyles.push({height: 34, width: 34}) - } else if (size === 'xsmall') { - baseStyles.push({height: 28, width: 28}) - } else if (size === 'tiny') { - baseStyles.push({height: 20, width: 20}) - } - - if (shape === 'round') { - baseStyles.push(a.rounded_full) - } else if (shape === 'square') { - if (size === 'tiny') { - baseStyles.push(a.rounded_xs) - } else { - baseStyles.push(a.rounded_sm) + baseStyles.push(a.rounded_full) + } else if (shape === 'square') { + if (size === 'tiny') { + baseStyles.push(a.rounded_xs) + } else { + baseStyles.push(a.rounded_sm) + } } } - } - - return { - baseStyles, - hoverStyles, - } - }, [t, variant, color, size, shape, disabled]) - - const {gradientColors, gradientHoverColors, gradientLocations} = - React.useMemo(() => { - const colors: string[] = [] - const hoverColors: string[] = [] - const locations: number[] = [] - const gradient = { - primary: tokens.gradients.sky, - secondary: tokens.gradients.sky, - negative: tokens.gradients.sky, - gradient_sky: tokens.gradients.sky, - gradient_midnight: tokens.gradients.midnight, - gradient_sunrise: tokens.gradients.sunrise, - gradient_sunset: tokens.gradients.sunset, - gradient_nordic: tokens.gradients.nordic, - gradient_bonfire: tokens.gradients.bonfire, - }[color || 'primary'] - - if (variant === 'gradient') { - colors.push(...gradient.values.map(([_, color]) => color)) - hoverColors.push(...gradient.values.map(_ => gradient.hover_value)) - locations.push(...gradient.values.map(([location, _]) => location)) - } return { - gradientColors: colors, - gradientHoverColors: hoverColors, - gradientLocations: locations, + baseStyles, + hoverStyles, } - }, [variant, color]) - - const context = React.useMemo( - () => ({ - ...state, - variant, - color, - size, - disabled: disabled || false, - }), - [state, variant, color, size, disabled], - ) - - const flattenedBaseStyles = flatten(baseStyles) + }, [t, variant, color, size, shape, disabled]) + + const {gradientColors, gradientHoverColors, gradientLocations} = + React.useMemo(() => { + const colors: string[] = [] + const hoverColors: string[] = [] + const locations: number[] = [] + const gradient = { + primary: tokens.gradients.sky, + secondary: tokens.gradients.sky, + negative: tokens.gradients.sky, + gradient_sky: tokens.gradients.sky, + gradient_midnight: tokens.gradients.midnight, + gradient_sunrise: tokens.gradients.sunrise, + gradient_sunset: tokens.gradients.sunset, + gradient_nordic: tokens.gradients.nordic, + gradient_bonfire: tokens.gradients.bonfire, + }[color || 'primary'] + + if (variant === 'gradient') { + colors.push(...gradient.values.map(([_, color]) => color)) + hoverColors.push(...gradient.values.map(_ => gradient.hover_value)) + locations.push(...gradient.values.map(([location, _]) => location)) + } - return ( - ( + () => ({ + ...state, + variant, + color, + size, disabled: disabled || false, - }} - style={[ - a.flex_row, - a.align_center, - a.justify_center, - flattenedBaseStyles, - flatten(style), - ...(state.hovered || state.pressed - ? [hoverStyles, flatten(hoverStyleProp)] - : []), - ]} - onPressIn={onPressIn} - onPressOut={onPressOut} - onHoverIn={onHoverIn} - onHoverOut={onHoverOut} - onFocus={onFocus} - onBlur={onBlur}> - {variant === 'gradient' && ( - - - - )} - - {typeof children === 'function' ? children(context) : children} - - - ) -} + }), + [state, variant, color, size, disabled], + ) + + const flattenedBaseStyles = flatten(baseStyles) + + return ( + + {variant === 'gradient' && ( + + + + )} + + {typeof children === 'function' ? children(context) : children} + + + ) + }, +) +Button.displayName = 'Button' export function useSharedButtonTextStyles() { const t = useTheme() diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index 63f61ce856..7b861dc660 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -100,7 +100,15 @@ function KnownFollowersInner({ moderation, } }) - const count = cachedKnownFollowers.count + + // Does not have blocks applied. Always >= slices.length + const serverCount = cachedKnownFollowers.count + + /* + * We check above too, but here for clarity and a reminder to _check for + * valid indices_ + */ + if (slice.length === 0) return null return ( - {count > 2 ? ( - - Followed by{' '} - - {slice[0].profile.displayName} - - ,{' '} - - {slice[1].profile.displayName} - - , and{' '} - - - ) : count === 2 ? ( + {slice.length >= 2 ? ( + // 2-n followers, including blocks + serverCount > 2 ? ( + + Followed by{' '} + + {slice[0].profile.displayName} + + ,{' '} + + {slice[1].profile.displayName} + + , and{' '} + + + ) : ( + // only 2 + + Followed by{' '} + + {slice[0].profile.displayName} + {' '} + and{' '} + + {slice[1].profile.displayName} + + + ) + ) : serverCount > 1 ? ( + // 1-n followers, including blocks Followed by{' '} {slice[0].profile.displayName} {' '} and{' '} - - {slice[1].profile.displayName} - + ) : ( + // only 1 Followed by{' '} diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 73a660ea6c..f7a827b493 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -196,6 +196,13 @@ export function createInput(Component: typeof TextInput) { textAlignVertical: rest.multiline ? 'top' : undefined, minHeight: rest.multiline ? 80 : undefined, }, + // fix for autofill styles covering border + web({ + paddingTop: 12, + paddingBottom: 12, + marginTop: 2, + marginBottom: 2, + }), android({ paddingBottom: 16, }), diff --git a/src/components/moderation/PostAlerts.tsx b/src/components/moderation/PostAlerts.tsx index 0b48b51d1d..ec7529a4ff 100644 --- a/src/components/moderation/PostAlerts.tsx +++ b/src/components/moderation/PostAlerts.tsx @@ -92,6 +92,8 @@ function PostLabel({ ) : ( diff --git a/src/components/moderation/PostHider.tsx b/src/components/moderation/PostHider.tsx index 8a64742978..b6fb174528 100644 --- a/src/components/moderation/PostHider.tsx +++ b/src/components/moderation/PostHider.tsx @@ -1,6 +1,6 @@ import React, {ComponentProps} from 'react' import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' -import {AppBskyActorDefs, ModerationUI} from '@atproto/api' +import {AppBskyActorDefs, ModerationCause, ModerationUI} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' @@ -45,7 +45,8 @@ export function PostHider({ const [override, setOverride] = React.useState(false) const control = useModerationDetailsDialogControl() const blur = - modui.blurs[0] || (interpretFilterAsBlur ? modui.filters[0] : undefined) + modui.blurs[0] || + (interpretFilterAsBlur ? getBlurrableFilter(modui) : undefined) const desc = useModerationCauseDescription(blur) const onBeforePress = React.useCallback(() => { @@ -134,6 +135,13 @@ export function PostHider({ ) } +function getBlurrableFilter(modui: ModerationUI): ModerationCause | undefined { + // moderation causes get "downgraded" when they originate from embedded content + // a downgraded cause should *only* drive filtering in feeds, so we want to look + // for filters that arent downgraded + return modui.filters.find(filter => !filter.downgraded) +} + const styles = StyleSheet.create({ child: { borderWidth: 0, diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 05d1591f56..e0b8998007 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -84,6 +84,7 @@ export const createHitslop = (size: number): Insets => ({ export const HITSLOP_10 = createHitslop(10) export const HITSLOP_20 = createHitslop(20) export const HITSLOP_30 = createHitslop(30) +export const POST_CTRL_HITSLOP = {top: 5, bottom: 10, left: 10, right: 10} export const BACK_HITSLOP = HITSLOP_30 export const MAX_POST_LINES = 25 diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index 30ced14921..44e42fae1c 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -1,7 +1,8 @@ -import {Dimensions, Platform} from 'react-native' +import {Dimensions} from 'react-native' import {isSafari} from 'lib/browser' import {isWeb} from 'platform/detection' + const {height: SCREEN_HEIGHT} = Dimensions.get('window') const IFRAME_HOST = isWeb @@ -342,40 +343,17 @@ export function parseEmbedPlayerFromUrl( } } - if (urlp.hostname === 'media.tenor.com') { - let [_, id, filename] = urlp.pathname.split('/') - - const h = urlp.searchParams.get('hh') - const w = urlp.searchParams.get('ww') - let dimensions - if (h && w) { - dimensions = { - height: Number(h), - width: Number(w), - } - } - - if (id && filename && dimensions && id.includes('AAAAC')) { - if (Platform.OS === 'web') { - if (isSafari) { - id = id.replace('AAAAC', 'AAAP1') - filename = filename.replace('.gif', '.mp4') - } else { - id = id.replace('AAAAC', 'AAAP3') - filename = filename.replace('.gif', '.webm') - } - } else { - id = id.replace('AAAAC', 'AAAAM') - } - - return { - type: 'tenor_gif', - source: 'tenor', - isGif: true, - hideDetails: true, - playerUri: `https://t.gifs.bsky.app/${id}/${filename}`, - dimensions, - } + const tenorGif = parseTenorGif(urlp) + if (tenorGif.success) { + const {playerUri, dimensions} = tenorGif + + return { + type: 'tenor_gif', + source: 'tenor', + isGif: true, + hideDetails: true, + playerUri, + dimensions, } } @@ -516,3 +494,55 @@ export function getGiphyMetaUri(url: URL) { } } } + +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) { + if (isSafari) { + id = id.replace('AAAAC', 'AAAP1') + filename = filename.replace('.gif', '.mp4') + } else { + 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, + } +} diff --git a/src/lib/strings/time.ts b/src/lib/strings/time.ts index 1194e0240c..bfefea9bc3 100644 --- a/src/lib/strings/time.ts +++ b/src/lib/strings/time.ts @@ -19,3 +19,14 @@ export function getAge(birthDate: Date): number { } return age } + +/** + * Compares two dates by year, month, and day only + */ +export function simpleAreDatesEqual(a: Date, b: Date): boolean { + return ( + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() + ) +} diff --git a/src/locale/i18n.ts b/src/locale/i18n.ts index baec4b8a2d..332b9309aa 100644 --- a/src/locale/i18n.ts +++ b/src/locale/i18n.ts @@ -1,6 +1,10 @@ -import '@formatjs/intl-locale/polyfill' -import '@formatjs/intl-pluralrules/polyfill-force' // Don't remove -force because detection is very slow +// Don't remove -force from these because detection is VERY slow on low-end Android. +// https://github.com/formatjs/formatjs/issues/4463#issuecomment-2176070577 +import '@formatjs/intl-locale/polyfill-force' +import '@formatjs/intl-pluralrules/polyfill-force' +import '@formatjs/intl-numberformat/polyfill-force' import '@formatjs/intl-pluralrules/locale-data/en' +import '@formatjs/intl-numberformat/locale-data/en' import {useEffect} from 'react' import {i18n} from '@lingui/core' diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index 3556bba7a0..5304aa5031 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -181,8 +181,8 @@ export function StepProfile() { image = await openCropper({ mediaType: 'photo', cropperCircleOverlay: true, - height: image.height, - width: image.width, + height: 1000, + width: 1000, path: image.path, }) } diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 6588eb2e1d..d266decb32 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -82,7 +82,7 @@ let ProfileHeaderLabeler = ({ preferences?.moderationPrefs.labelers.find(l => l.did === profile.did) const canSubscribe = isSubscribed || - (preferences ? preferences?.moderationPrefs.labelers.length < 9 : false) + (preferences ? preferences?.moderationPrefs.labelers.length <= 20 : false) const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation() const {mutateAsync: unlikeMod, isPending: isUnlikePending} = useUnlikeMutation() @@ -328,8 +328,8 @@ function CantSubscribePrompt({ Unable to subscribe - We're sorry! You can only subscribe to ten labelers, and you've - reached your limit of ten. + We're sorry! You can only subscribe to twenty labelers, and you've + reached your limit of twenty. diff --git a/src/screens/Profile/Header/index.tsx b/src/screens/Profile/Header/index.tsx index 1280dd8b10..c7ef34b701 100644 --- a/src/screens/Profile/Header/index.tsx +++ b/src/screens/Profile/Header/index.tsx @@ -6,11 +6,11 @@ import { ModerationOpts, RichText as RichTextAPI, } from '@atproto/api' -import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' -import {usePalette} from 'lib/hooks/usePalette' -import {ProfileHeaderStandard} from './ProfileHeaderStandard' +import {usePalette} from 'lib/hooks/usePalette' +import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {ProfileHeaderLabeler} from './ProfileHeaderLabeler' +import {ProfileHeaderStandard} from './ProfileHeaderStandard' let ProfileHeaderLoading = (_props: {}): React.ReactNode => { const pal = usePalette('default') @@ -19,11 +19,11 @@ let ProfileHeaderLoading = (_props: {}): React.ReactNode => { - + - + @@ -58,13 +58,13 @@ const styles = StyleSheet.create({ position: 'absolute', top: 110, left: 10, - width: 84, - height: 84, - borderRadius: 42, + width: 94, + height: 94, + borderRadius: 47, borderWidth: 2, }, content: { - paddingTop: 8, + paddingTop: 12, paddingHorizontal: 14, paddingBottom: 4, }, @@ -73,6 +73,6 @@ const styles = StyleSheet.create({ marginLeft: 'auto', marginBottom: 12, }, - br40: {borderRadius: 40}, + br45: {borderRadius: 45}, br50: {borderRadius: 50}, }) diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts index facc680bd7..87700cb88e 100644 --- a/src/screens/Signup/state.ts +++ b/src/screens/Signup/state.ts @@ -252,7 +252,6 @@ export function useSubmitSignup({ dispatch({type: 'setIsLoading', value: true}) try { - onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view await createAccount({ service: state.serviceUrl, email: state.email, @@ -262,8 +261,12 @@ export function useSubmitSignup({ inviteCode: state.inviteCode.trim(), verificationCode: verificationCode, }) + /* + * Must happen last so that if the user has multiple tabs open and + * createAccount fails, one tab is not stuck in onboarding — Eric + */ + onboardingDispatch({type: 'start'}) } catch (e: any) { - onboardingDispatch({type: 'skip'}) // undo starting the onboard let errMsg = e.toString() if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { dispatch({ diff --git a/src/state/a11y.tsx b/src/state/a11y.tsx new file mode 100644 index 0000000000..aefcfd1ec4 --- /dev/null +++ b/src/state/a11y.tsx @@ -0,0 +1,65 @@ +import React from 'react' +import {AccessibilityInfo} from 'react-native' +import {isReducedMotion} from 'react-native-reanimated' + +import {isWeb} from '#/platform/detection' + +const Context = React.createContext({ + reduceMotionEnabled: false, + screenReaderEnabled: false, +}) + +export function useA11y() { + return React.useContext(Context) +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [reduceMotionEnabled, setReduceMotionEnabled] = React.useState(() => + isReducedMotion(), + ) + const [screenReaderEnabled, setScreenReaderEnabled] = React.useState(false) + + React.useEffect(() => { + const reduceMotionChangedSubscription = AccessibilityInfo.addEventListener( + 'reduceMotionChanged', + enabled => { + setReduceMotionEnabled(enabled) + }, + ) + const screenReaderChangedSubscription = AccessibilityInfo.addEventListener( + 'screenReaderChanged', + enabled => { + setScreenReaderEnabled(enabled) + }, + ) + + ;(async () => { + const [_reduceMotionEnabled, _screenReaderEnabled] = await Promise.all([ + AccessibilityInfo.isReduceMotionEnabled(), + AccessibilityInfo.isScreenReaderEnabled(), + ]) + setReduceMotionEnabled(_reduceMotionEnabled) + setScreenReaderEnabled(_screenReaderEnabled) + })() + + return () => { + reduceMotionChangedSubscription.remove() + screenReaderChangedSubscription.remove() + } + }, []) + + const ctx = React.useMemo(() => { + return { + reduceMotionEnabled, + /** + * Always returns true on web. For now, we're using this for mobile a11y, + * so we reset to false on web. + * + * @see https://github.com/necolas/react-native-web/discussions/2072 + */ + screenReaderEnabled: isWeb ? false : screenReaderEnabled, + } + }, [reduceMotionEnabled, screenReaderEnabled]) + + return {children} +} diff --git a/src/state/cache/thread-mutes.tsx b/src/state/cache/thread-mutes.tsx index b58bd430f5..dc5104c140 100644 --- a/src/state/cache/thread-mutes.tsx +++ b/src/state/cache/thread-mutes.tsx @@ -1,4 +1,7 @@ -import React from 'react' +import React, {useEffect} from 'react' + +import * as persisted from '#/state/persisted' +import {useAgent, useSession} from '../session' type StateContext = Map type SetStateContext = (uri: string, value: boolean) => void @@ -21,6 +24,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }, [setState], ) + + useMigrateMutes(setThreadMute) + return ( @@ -42,3 +48,50 @@ export function useIsThreadMuted(uri: string, defaultValue = false) { export function useSetThreadMute() { return React.useContext(setStateContext) } + +function useMigrateMutes(setThreadMute: SetStateContext) { + const agent = useAgent() + const {currentAccount} = useSession() + + useEffect(() => { + if (currentAccount) { + if ( + !persisted + .get('mutedThreads') + .some(uri => uri.includes(currentAccount.did)) + ) { + return + } + + let cancelled = false + + const migrate = async () => { + while (!cancelled) { + const threads = persisted.get('mutedThreads') + + const root = threads.findLast(uri => uri.includes(currentAccount.did)) + + if (!root) break + + persisted.write( + 'mutedThreads', + threads.filter(uri => uri !== root), + ) + + setThreadMute(root, true) + + await agent.api.app.bsky.graph + .muteThread({root}) + // not a big deal if this fails, since the post might have been deleted + .catch(console.error) + } + } + + migrate() + + return () => { + cancelled = true + } + } + }, [agent, currentAccount, setThreadMute]) +} diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index b81cf5962d..c942828f2a 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -60,6 +60,7 @@ export const schema = z.object({ appLanguage: z.string(), }), requireAltTextEnabled: z.boolean(), // should move to server + largeAltBadgeEnabled: z.boolean().optional(), externalEmbeds: z .object({ giphy: z.enum(externalEmbedOptions).optional(), @@ -74,7 +75,6 @@ export const schema = z.object({ flickr: z.enum(externalEmbedOptions).optional(), }) .optional(), - mutedThreads: z.array(z.string()), // should move to server invites: z.object({ copiedInvites: z.array(z.string()), }), @@ -88,6 +88,8 @@ export const schema = z.object({ disableHaptics: z.boolean().optional(), disableAutoplay: z.boolean().optional(), kawaii: z.boolean().optional(), + /** @deprecated */ + mutedThreads: z.array(z.string()), }) export type Schema = z.infer @@ -111,6 +113,7 @@ export const defaults: Schema = { appLanguage: deviceLocales[0] || 'en', }, requireAltTextEnabled: false, + largeAltBadgeEnabled: false, externalEmbeds: {}, mutedThreads: [], invites: { diff --git a/src/state/preferences/alt-text-required.tsx b/src/state/preferences/alt-text-required.tsx index 81de9e0060..642e790fbc 100644 --- a/src/state/preferences/alt-text-required.tsx +++ b/src/state/preferences/alt-text-required.tsx @@ -1,4 +1,5 @@ import React from 'react' + import * as persisted from '#/state/persisted' type StateContext = persisted.Schema['requireAltTextEnabled'] diff --git a/src/state/preferences/hidden-posts.tsx b/src/state/preferences/hidden-posts.tsx index 11119ce758..2c6a373e15 100644 --- a/src/state/preferences/hidden-posts.tsx +++ b/src/state/preferences/hidden-posts.tsx @@ -1,4 +1,5 @@ import React from 'react' + import * as persisted from '#/state/persisted' type SetStateCb = ( diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index 70c8efc805..e1a35f193c 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -8,6 +8,7 @@ import {Provider as HiddenPostsProvider} from './hidden-posts' import {Provider as InAppBrowserProvider} from './in-app-browser' import {Provider as KawaiiProvider} from './kawaii' import {Provider as LanguagesProvider} from './languages' +import {Provider as LargeAltBadgeProvider} from './large-alt-badge' export { useRequireAltTextEnabled, @@ -27,17 +28,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return ( - - - - - - {children} - - - - - + + + + + + + {children} + + + + + + ) diff --git a/src/state/preferences/large-alt-badge.tsx b/src/state/preferences/large-alt-badge.tsx new file mode 100644 index 0000000000..b3d597c5cb --- /dev/null +++ b/src/state/preferences/large-alt-badge.tsx @@ -0,0 +1,49 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['largeAltBadgeEnabled'] +type SetContext = (v: persisted.Schema['largeAltBadgeEnabled']) => void + +const stateContext = React.createContext( + persisted.defaults.largeAltBadgeEnabled, +) +const setContext = React.createContext( + (_: persisted.Schema['largeAltBadgeEnabled']) => {}, +) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [state, setState] = React.useState( + persisted.get('largeAltBadgeEnabled'), + ) + + const setStateWrapped = React.useCallback( + (largeAltBadgeEnabled: persisted.Schema['largeAltBadgeEnabled']) => { + setState(largeAltBadgeEnabled) + persisted.write('largeAltBadgeEnabled', largeAltBadgeEnabled) + }, + [setState], + ) + + React.useEffect(() => { + return persisted.onUpdate(() => { + setState(persisted.get('largeAltBadgeEnabled')) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useLargeAltBadgeEnabled() { + return React.useContext(stateContext) +} + +export function useSetLargeAltBadgeEnabled() { + return React.useContext(setContext) +} diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 2fb80de37d..4e44c1c695 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -78,6 +78,7 @@ export interface FeedPostSliceItem { feedContext: string | undefined moderation: ModerationDecision parentAuthor?: AppBskyActorDefs.ProfileViewBasic + isParentBlocked?: boolean } export interface FeedPostSlice { @@ -311,6 +312,10 @@ export function usePostFeedQuery( const parentAuthor = item.reply?.parent?.author ?? slice.items[i + 1]?.reply?.grandparentAuthor + const replyRef = item.reply + const isParentBlocked = AppBskyFeedDefs.isBlockedPost( + replyRef?.parent, + ) return { _reactKey: `${slice._reactKey}-${i}-${item.post.uri}`, @@ -324,6 +329,7 @@ export function usePostFeedQuery( feedContext: item.feedContext || slice.feedContext, moderation: moderations[i], parentAuthor, + isParentBlocked, } } return undefined diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 8e77bf6b92..a511d6b3d7 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -304,8 +304,8 @@ export function useThreadMuteMutationQueue( const queueToggle = useToggleMutationQueue({ initialState: isThreadMuted, - runMutation: async (_prev, shouldLike) => { - if (shouldLike) { + runMutation: async (_prev, shouldMute) => { + if (shouldMute) { await threadMuteMutation.mutateAsync({ uri: rootUri, }) diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 48f5614bd8..5a58937faa 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -11,6 +11,7 @@ import { import {tryFetchGates} from '#/lib/statsig/statsig' import {getAge} from '#/lib/strings/time' import {logger} from '#/logger' +import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' import { configureModerationForAccount, configureModerationForGuest, @@ -37,21 +38,7 @@ export async function createAgentAndResume( } const gates = tryFetchGates(storedAccount.did, 'prefer-low-latency') const moderation = configureModerationForAccount(agent, storedAccount) - const prevSession: AtpSessionData = { - // Sorted in the same property order as when returned by BskyAgent (alphabetical). - accessJwt: storedAccount.accessJwt ?? '', - did: storedAccount.did, - email: storedAccount.email, - emailAuthFactor: storedAccount.emailAuthFactor, - emailConfirmed: storedAccount.emailConfirmed, - handle: storedAccount.handle, - refreshJwt: storedAccount.refreshJwt ?? '', - /** - * @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188 - */ - active: storedAccount.active ?? true, - status: storedAccount.status, - } + const prevSession: AtpSessionData = sessionAccountToSession(storedAccount) if (isSessionExpired(storedAccount)) { await networkRetry(1, () => agent.resumeSession(prevSession)) } else { @@ -191,6 +178,13 @@ export async function createAgentAndCreateAccount( agent.setPersonalDetails({birthDate: birthDate.toISOString()}) } + try { + // snooze first prompt after signup, defer to next prompt + snoozeEmailConfirmationPrompt() + } catch (e: any) { + logger.error(e, {context: `session: failed snoozeEmailConfirmationPrompt`}) + } + return prepareAgent(agent, gates, moderation, onSessionChange) } @@ -245,3 +239,23 @@ export function agentToSessionAccount( pdsUrl: agent.pdsUrl?.toString(), } } + +export function sessionAccountToSession( + account: SessionAccount, +): AtpSessionData { + return { + // Sorted in the same property order as when returned by BskyAgent (alphabetical). + accessJwt: account.accessJwt ?? '', + did: account.did, + email: account.email, + emailAuthFactor: account.emailAuthFactor, + emailConfirmed: account.emailConfirmed, + handle: account.handle, + refreshJwt: account.refreshJwt ?? '', + /** + * @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188 + */ + active: account.active ?? true, + status: account.status, + } +} diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 371bd459ad..314945bcf9 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -14,6 +14,7 @@ import { createAgentAndCreateAccount, createAgentAndLogin, createAgentAndResume, + sessionAccountToSession, } from './agent' import {getInitialState, reducer} from './reducer' @@ -175,8 +176,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { if (syncedAccount.did !== state.currentAgentState.did) { resumeSession(syncedAccount) } else { - // @ts-ignore we checked for `refreshJwt` above - state.currentAgentState.agent.session = syncedAccount + const agent = state.currentAgentState.agent as BskyAgent + agent.session = sessionAccountToSession(syncedAccount) } } }) diff --git a/src/state/shell/reminders.e2e.ts b/src/state/shell/reminders.e2e.ts index e8c12792ac..94809a680d 100644 --- a/src/state/shell/reminders.e2e.ts +++ b/src/state/shell/reminders.e2e.ts @@ -1,7 +1,5 @@ -export function init() {} - export function shouldRequestEmailConfirmation() { return false } -export function setEmailConfirmationRequested() {} +export function snoozeEmailConfirmationPrompt() {} diff --git a/src/state/shell/reminders.ts b/src/state/shell/reminders.ts index ee924eb001..db6ee9391b 100644 --- a/src/state/shell/reminders.ts +++ b/src/state/shell/reminders.ts @@ -1,36 +1,45 @@ +import {simpleAreDatesEqual} from '#/lib/strings/time' +import {logger} from '#/logger' import * as persisted from '#/state/persisted' -import {toHashCode} from 'lib/strings/helpers' -import {isOnboardingActive} from './onboarding' import {SessionAccount} from '../session' +import {isOnboardingActive} from './onboarding' export function shouldRequestEmailConfirmation(account: SessionAccount) { - if (!account) { - return false - } - if (account.emailConfirmed) { - return false - } - if (isOnboardingActive()) { - return false - } - // only prompt once - if (persisted.get('reminders').lastEmailConfirm) { - return false - } + // ignore logged out + if (!account) return false + // ignore confirmed accounts, this is the success state of this reminder + if (account.emailConfirmed) return false + // wait for onboarding to complete + if (isOnboardingActive()) return false + + const snoozedAt = persisted.get('reminders').lastEmailConfirm const today = new Date() - // shard the users into 2 day of the week buckets - // (this is to avoid a sudden influx of email updates when - // this feature rolls out) - const code = toHashCode(account.did) % 7 - if (code !== today.getDay() && code !== (today.getDay() + 1) % 7) { + + logger.debug('Checking email confirmation reminder', { + today, + snoozedAt, + }) + + // never been snoozed, new account + if (!snoozedAt) { + return true + } + + // already snoozed today + if (simpleAreDatesEqual(new Date(Date.parse(snoozedAt)), new Date())) { return false } + return true } -export function setEmailConfirmationRequested() { +export function snoozeEmailConfirmationPrompt() { + const lastEmailConfirm = new Date().toISOString() + logger.debug('Snoozing email confirmation reminder', { + snoozedAt: lastEmailConfirm, + }) persisted.write('reminders', { ...persisted.get('reminders'), - lastEmailConfirm: new Date().toISOString(), + lastEmailConfirm, }) } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 80bce5351c..9e2f77d4df 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -24,12 +24,18 @@ import Animated, { } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {LinearGradient} from 'expo-linear-gradient' +import { + AppBskyFeedDefs, + AppBskyFeedGetPostThread, + BskyAgent, +} from '@atproto/api' import {RichText} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {observer} from 'mobx-react-lite' +import {until} from '#/lib/async/until' import { createGIFDescription, parseAltFromGIFDescription, @@ -299,6 +305,17 @@ export const ComposePost = observer(function ComposePost({ langs: toPostLanguages(langPrefs.postLanguage), }) ).uri + try { + await whenAppViewReady(agent, postUri, res => { + const thread = res.data.thread + return AppBskyFeedDefs.isThreadViewPost(thread) + }) + } catch (waitErr: any) { + logger.error(waitErr, { + message: `Waiting for app view failed`, + }) + // Keep going because the post *was* published. + } } catch (e: any) { logger.error(e, { message: `Composer: create post failed`, @@ -756,6 +773,23 @@ function useKeyboardVerticalOffset() { return top + 10 } +async function whenAppViewReady( + agent: BskyAgent, + uri: string, + fn: (res: AppBskyFeedGetPostThread.Response) => boolean, +) { + await until( + 5, // 5 tries + 1e3, // 1s delay between tries + fn, + () => + agent.app.bsky.feed.getPostThread({ + uri, + depth: 0, + }), + ) +} + const styles = StyleSheet.create({ topbarInner: { flexDirection: 'row', diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index 895baa9a4d..8cf0452cec 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -120,6 +120,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingVertical: 5, width: '100%', + minHeight: 46, }, title: { fontSize: 21, diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index d6c38ea61c..9cd7a29176 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -8,6 +8,7 @@ import { } from 'react-native' import { AppBskyActorDefs, + AppBskyEmbedExternal, AppBskyEmbedImages, AppBskyEmbedRecordWithMedia, AppBskyFeedDefs, @@ -51,6 +52,7 @@ import {TimeElapsed} from '../util/TimeElapsed' import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar' import hairlineWidth = StyleSheet.hairlineWidth +import {parseTenorGif} from '#/lib/strings/embed-player' const MAX_AUTHORS = 5 @@ -465,17 +467,48 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) { const pal = usePalette('default') if (post && AppBskyFeedPost.isRecord(post?.record)) { const text = post.record.text - const images = AppBskyEmbedImages.isView(post.embed) - ? post.embed.images - : AppBskyEmbedRecordWithMedia.isView(post.embed) && - AppBskyEmbedImages.isView(post.embed.media) - ? post.embed.media.images - : undefined + let images + let isGif = false + + if (AppBskyEmbedImages.isView(post.embed)) { + images = post.embed.images + } else if ( + AppBskyEmbedRecordWithMedia.isView(post.embed) && + AppBskyEmbedImages.isView(post.embed.media) + ) { + images = post.embed.media.images + } else if ( + AppBskyEmbedExternal.isView(post.embed) && + post.embed.external.thumb + ) { + let url: URL | undefined + try { + url = new URL(post.embed.external.uri) + } catch {} + if (url) { + const {success} = parseTenorGif(url) + if (success) { + isGif = true + images = [ + { + thumb: post.embed.external.thumb, + alt: post.embed.external.title, + fullsize: post.embed.external.thumb, + }, + ] + } + } + } + return ( <> {text?.length > 0 && {text}} {images && images.length > 0 && ( - + )} ) diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index e940e8d1a9..1c83ecd6e2 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -180,7 +180,7 @@ const desktopStyles = StyleSheet.create({ position: 'absolute', left: 0, right: 0, - bottom: -1, + top: '100%', borderBottomWidth: 1, }, }) @@ -207,7 +207,7 @@ const mobileStyles = StyleSheet.create({ position: 'absolute', left: 0, right: 0, - bottom: -1, + top: '100%', borderBottomWidth: hairlineWidth, }, }) diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 8061eb11c9..a6c1a46487 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -331,7 +331,11 @@ export function PostThread({ - setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider) + setHiddenRepliesState( + item === SHOW_HIDDEN_REPLIES + ? HiddenRepliesState.Show + : HiddenRepliesState.ShowAndOverridePostHider, + ) } hideTopBorder={index === 0} /> diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 6d03029d7f..92b529db78 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -25,7 +25,7 @@ import {sanitizeHandle} from 'lib/strings/handles' import {countLines} from 'lib/strings/helpers' import {niceDate} from 'lib/strings/time' import {s} from 'lib/styles' -import {isNative, isWeb} from 'platform/detection' +import {isWeb} from 'platform/detection' import {useSession} from 'state/session' import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' import {atoms as a} from '#/alf' @@ -35,7 +35,7 @@ import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' import {PostAlerts} from '../../../components/moderation/PostAlerts' import {PostHider} from '../../../components/moderation/PostHider' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' -import {WhoCanReply} from '../threadgate/WhoCanReply' +import {WhoCanReplyBlock, WhoCanReplyInline} from '../threadgate/WhoCanReply' import {ErrorMessage} from '../util/error/ErrorMessage' import {Link, TextLink} from '../util/Link' import {formatCount} from '../util/numeric/format' @@ -340,6 +340,7 @@ let PostThreadItemLoaded = ({ @@ -396,11 +397,6 @@ let PostThreadItemLoaded = ({ - ) } else { @@ -579,14 +575,7 @@ let PostThreadItemLoaded = ({ ) : undefined} - + ) } @@ -654,10 +643,12 @@ function PostOuterWrapper({ function ExpandedPostDetails({ post, + isThreadAuthor, needsTranslation, translatorUrl, }: { post: AppBskyFeedDefs.PostView + isThreadAuthor: boolean needsTranslation: boolean translatorUrl: string }) { @@ -670,14 +661,23 @@ function ExpandedPostDetails({ }, [openLink, translatorUrl]) return ( - - {niceDate(post.indexedAt)} + + {niceDate(post.indexedAt)} + {needsTranslation && ( <> - · + · Translate diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 675f23a88c..cc767a4a3d 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -56,6 +56,7 @@ interface FeedItemProps { isThreadParent?: boolean feedContext: string | undefined hideTopBorder?: boolean + isParentBlocked?: boolean } export function FeedItem({ @@ -70,6 +71,7 @@ export function FeedItem({ isThreadLastChild, isThreadParent, hideTopBorder, + isParentBlocked, }: FeedItemProps & {post: AppBskyFeedDefs.PostView}): React.ReactNode { const postShadowed = usePostShadow(post) const richText = useMemo( @@ -100,6 +102,7 @@ export function FeedItem({ isThreadLastChild={isThreadLastChild} isThreadParent={isThreadParent} hideTopBorder={hideTopBorder} + isParentBlocked={isParentBlocked} /> ) } @@ -119,6 +122,7 @@ let FeedItemInner = ({ isThreadLastChild, isThreadParent, hideTopBorder, + isParentBlocked, }: FeedItemProps & { richText: RichTextAPI post: Shadow @@ -320,7 +324,7 @@ let FeedItemInner = ({ onOpenAuthor={onOpenAuthor} /> {!isThreadChild && showReplyTo && parentAuthor && ( - + )} - - Reply to{' '} - - - - + {blocked ? ( + Reply to a blocked post + ) : ( + + Reply to{' '} + + + + + )} ) diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx index aeb24e8bbf..3e08f253cf 100644 --- a/src/view/com/posts/FeedSlice.tsx +++ b/src/view/com/posts/FeedSlice.tsx @@ -34,6 +34,7 @@ let FeedSlice = ({ isThreadParent={isThreadParentAt(slice.items, 0)} isThreadChild={isThreadChildAt(slice.items, 0)} hideTopBorder={hideTopBorder} + isParentBlocked={slice.items[0].isParentBlocked} /> @@ -82,6 +85,7 @@ let FeedSlice = ({ isThreadLastChild={ isThreadChildAt(slice.items, i) && slice.items.length === i + 1 } + isParentBlocked={slice.items[i].isParentBlocked} hideTopBorder={hideTopBorder && i === 0} /> ))} diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 2b0790002d..a3cd5ca1b9 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -328,6 +328,7 @@ const styles = StyleSheet.create({ borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2, + justifyContent: 'center', }, btn: { paddingVertical: 7, diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index efc2497600..f5e050d707 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -9,7 +9,6 @@ import {useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' import {useAnalytics} from 'lib/analytics/analytics' import {HITSLOP_10} from 'lib/constants' -import {usePalette} from 'lib/hooks/usePalette' import {makeProfileLink} from 'lib/routes/links' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' @@ -24,7 +23,7 @@ import { import {useSession} from 'state/session' import {EventStopper} from 'view/com/util/EventStopper' import * as Toast from 'view/com/util/Toast' -import {useTheme} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle' @@ -49,7 +48,7 @@ let ProfileMenu = ({ const {currentAccount, hasSession} = useSession() const t = useTheme() // TODO ALF this - const pal = usePalette('default') + const alf = useTheme() const {track} = useAnalytics() const {openModal} = useModalControls() const reportDialogControl = useReportDialogControl() @@ -187,21 +186,21 @@ let ProfileMenu = ({ - {({props}) => { + {({props, state}) => { return ( -}) { - const {track} = useAnalytics() +} + +export function WhoCanReplyInline({ + post, + isThreadAuthor, + style, +}: WhoCanReplyProps) { const {_} = useLingui() - const pal = usePalette('default') - const agent = useAgent() - const queryClient = useQueryClient() - const {openModal} = useModalControls() - const containerStyles = useColorSchemeStyle( - { - backgroundColor: pal.colors.unreadNotifBg, - }, - { - backgroundColor: pal.colors.unreadNotifBg, - }, - ) - const textStyles = useColorSchemeStyle( - {color: colors.blue5}, - {color: colors.blue1}, - ) - const hoverStyles = useColorSchemeStyle( - { - backgroundColor: colors.white, - }, - { - backgroundColor: pal.colors.background, - }, - ) - const settings = React.useMemo( - () => threadgateViewToSettings(post.threadgate), - [post], - ) - const isRootPost = !('reply' in post.record) + const t = useTheme() + const infoDialogControl = useDialogControl() + const {settings, isRootPost, onPressEdit} = useWhoCanReply(post) - const onPressEdit = () => { - track('Post:EditThreadgateOpened') - if (isNative && Keyboard.isVisible()) { - Keyboard.dismiss() - } - openModal({ - name: 'threadgate', - settings, - async onConfirm(newSettings: ThreadgateSetting[]) { - try { - if (newSettings.length) { - await createThreadgate(agent, post.uri, newSettings) - } else { - await agent.api.com.atproto.repo.deleteRecord({ - repo: agent.session!.did, - collection: 'app.bsky.feed.threadgate', - rkey: new AtUri(post.uri).rkey, - }) - } - Toast.show('Thread settings updated') - queryClient.invalidateQueries({ - queryKey: [POST_THREAD_RQKEY_ROOT], - }) - track('Post:ThreadgateEdited') - } catch (err) { - Toast.show( - 'There was an issue. Please check your internet connection and try again.', - ) - logger.error('Failed to edit threadgate', {message: err}) - } - }, - }) + if (!isRootPost) { + return null } + if (!settings.length && !isThreadAuthor) { + return null + } + + const isEverybody = settings.length === 0 + const isNobody = !!settings.find(gate => gate.type === 'nobody') + const description = isEverybody + ? _(msg`Everybody can reply`) + : isNobody + ? _(msg`Replies disabled`) + : _(msg`Some people can reply`) + + return ( + <> + + + + ) +} + +export function WhoCanReplyBlock({ + post, + isThreadAuthor, + style, +}: WhoCanReplyProps) { + const {_} = useLingui() + const t = useTheme() + const infoDialogControl = useDialogControl() + const {settings, isRootPost, onPressEdit} = useWhoCanReply(post) if (!isRootPost) { return null @@ -107,65 +115,144 @@ export function WhoCanReply({ return null } + const isEverybody = settings.length === 0 + const isNobody = !!settings.find(gate => gate.type === 'nobody') + const description = isEverybody + ? _(msg`Everybody can reply`) + : isNobody + ? _(msg`Replies on this thread are disabled`) + : _(msg`Some people can reply`) + return ( - - - - {!settings.length ? ( - Everybody can reply. - ) : settings[0].type === 'nobody' ? ( - Replies to this thread are disabled. - ) : ( - - Only{' '} - {settings.map((rule, i) => ( - <> - - - - ))}{' '} - can reply. - - )} + <> + + + + ) +} + +function Icon({ + color, + width, + settings, +}: { + color: string + width?: number + settings: ThreadgateSetting[] +}) { + const isEverybody = settings.length === 0 + const isNobody = !!settings.find(gate => gate.type === 'nobody') + const IconComponent = isEverybody ? Earth : isNobody ? CircleBanSign : Group + return +} + +function InfoDialog({ + control, + post, + settings, +}: { + control: Dialog.DialogControlProps + post: AppBskyFeedDefs.PostView + settings: ThreadgateSetting[] +}) { + return ( + + + + + ) +} + +function InfoDialogInner({ + post, + settings, +}: { + post: AppBskyFeedDefs.PostView + settings: ThreadgateSetting[] +}) { + const {_} = useLingui() + return ( + + + + Who can reply? + - {isThreadAuthor && ( - - - + + ) +} + +function Rules({ + post, + settings, +}: { + post: AppBskyFeedDefs.PostView + settings: ThreadgateSetting[] +}) { + const t = useTheme() + return ( + + {!settings.length ? ( + Everybody can reply + ) : settings[0].type === 'nobody' ? ( + Replies to this thread are disabled + ) : ( + + Only{' '} + {settings.map((rule, i) => ( + <> + + + + ))}{' '} + can reply + )} - + ) } @@ -178,7 +265,7 @@ function Rule({ post: AppBskyFeedDefs.PostView lists: AppBskyGraphDefs.ListViewBasic[] | undefined }) { - const pal = usePalette('default') + const t = useTheme() if (rule.type === 'mention') { return mentioned users } @@ -190,7 +277,7 @@ function Rule({ type="sm" href={makeProfileLink(post.author)} text={`@${post.author.handle}`} - style={pal.link} + style={{color: t.palette.primary_500}} /> ) @@ -205,7 +292,7 @@ function Rule({ type="sm" href={makeListLink(listUrip.hostname, listUrip.rkey)} text={list.name} - style={pal.link} + style={{color: t.palette.primary_500}} />{' '} members @@ -227,3 +314,78 @@ function Separator({i, length}: {i: number; length: number}) { } return <>, } + +function useWhoCanReply(post: AppBskyFeedDefs.PostView) { + const agent = useAgent() + const queryClient = useQueryClient() + const {openModal} = useModalControls() + + const settings = React.useMemo( + () => threadgateViewToSettings(post.threadgate), + [post], + ) + const isRootPost = !('reply' in post.record) + + const onPressEdit = () => { + if (isNative && Keyboard.isVisible()) { + Keyboard.dismiss() + } + openModal({ + name: 'threadgate', + settings, + async onConfirm(newSettings: ThreadgateSetting[]) { + try { + if (newSettings.length) { + await createThreadgate(agent, post.uri, newSettings) + } else { + await agent.api.com.atproto.repo.deleteRecord({ + repo: agent.session!.did, + collection: 'app.bsky.feed.threadgate', + rkey: new AtUri(post.uri).rkey, + }) + } + await whenAppViewReady(agent, post.uri, res => { + const thread = res.data.thread + if (AppBskyFeedDefs.isThreadViewPost(thread)) { + const fetchedSettings = threadgateViewToSettings( + thread.post.threadgate, + ) + return ( + JSON.stringify(fetchedSettings) === JSON.stringify(newSettings) + ) + } + return false + }) + Toast.show('Thread settings updated') + queryClient.invalidateQueries({ + queryKey: [POST_THREAD_RQKEY_ROOT], + }) + } catch (err) { + Toast.show( + 'There was an issue. Please check your internet connection and try again.', + ) + logger.error('Failed to edit threadgate', {message: err}) + } + }, + }) + } + + return {settings, isRootPost, onPressEdit} +} + +async function whenAppViewReady( + agent: BskyAgent, + uri: string, + fn: (res: AppBskyFeedGetPostThread.Response) => boolean, +) { + await until( + 5, // 5 tries + 1e3, // 1s delay between tries + fn, + () => + agent.app.bsky.feed.getPostThread({ + uri, + depth: 0, + }), + ) +} diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 6b0c17762c..e917ab1d32 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -38,6 +38,7 @@ function ListImpl( { ListHeaderComponent, ListFooterComponent, + ListEmptyComponent, containWeb, contentContainerStyle, data, @@ -72,23 +73,35 @@ function ListImpl( ) } - let header: JSX.Element | null = null + const isEmpty = !data || data.length === 0 + + let headerComponent: JSX.Element | null = null if (ListHeaderComponent != null) { if (isValidElement(ListHeaderComponent)) { - header = ListHeaderComponent + headerComponent = ListHeaderComponent } else { // @ts-ignore Nah it's fine. - header = + headerComponent = } } - let footer: JSX.Element | null = null + let footerComponent: JSX.Element | null = null if (ListFooterComponent != null) { if (isValidElement(ListFooterComponent)) { - footer = ListFooterComponent + footerComponent = ListFooterComponent + } else { + // @ts-ignore Nah it's fine. + footerComponent = + } + } + + let emptyComponent: JSX.Element | null = null + if (ListEmptyComponent != null) { + if (isValidElement(ListEmptyComponent)) { + emptyComponent = ListEmptyComponent } else { // @ts-ignore Nah it's fine. - footer = + emptyComponent = } } @@ -323,36 +336,38 @@ function ListImpl( onVisibleChange={handleAboveTheFoldVisibleChange} style={[styles.aboveTheFoldDetector, {height: headerOffset}]} /> - {onStartReached && ( + {onStartReached && !isEmpty && ( )} - {header} - {(data as Array).map((item, index) => { - const key = keyExtractor!(item, index) - return ( - - key={key} - item={item} - index={index} - renderItem={renderItem} - extraData={extraData} - onItemSeen={onItemSeen} - disableContentVisibility={disableContentVisibility} - /> - ) - })} - {onEndReached && ( + {headerComponent} + {isEmpty + ? emptyComponent + : (data as Array)?.map((item, index) => { + const key = keyExtractor!(item, index) + return ( + + key={key} + item={item} + index={index} + renderItem={renderItem} + extraData={extraData} + onItemSeen={onItemSeen} + disableContentVisibility={disableContentVisibility} + /> + ) + })} + {onEndReached && !isEmpty && ( )} - {footer} + {footerComponent} ) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 587b466a3c..e9aa625806 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -35,6 +35,7 @@ export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler' interface BaseUserAvatarProps { type?: UserAvatarType + shape?: 'circle' | 'square' size: number avatar?: string | null } @@ -60,12 +61,16 @@ const BLUR_AMOUNT = isWeb ? 5 : 100 let DefaultAvatar = ({ type, + shape: overrideShape, size, }: { type: UserAvatarType + shape?: 'square' | 'circle' size: number }): React.ReactNode => { + const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square') if (type === 'algo') { + // TODO: shape=circle // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. return ( - + {finalShape === 'square' ? ( + + ) : ( + + )} ) } + // TODO: shape=square return ( { const pal = usePalette('default') const backgroundColor = pal.colors.backgroundLight + const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square') const aviStyle = useMemo(() => { - if (type === 'algo' || type === 'list' || type === 'labeler') { + if (finalShape === 'square') { return { width: size, height: size, @@ -182,7 +195,7 @@ let UserAvatar = ({ borderRadius: Math.floor(size / 2), backgroundColor, } - }, [type, size, backgroundColor]) + }, [finalShape, size, backgroundColor]) const alert = useMemo(() => { if (!moderation?.alert) { @@ -224,7 +237,7 @@ let UserAvatar = ({ ) : ( - + {alert} ) @@ -290,8 +303,8 @@ let EditableUserAvatar = ({ const croppedImage = await openCropper({ mediaType: 'photo', cropperCircleOverlay: true, - height: item.height, - width: item.width, + height: 1000, + width: 1000, path: item.path, }) diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 8d23d258f5..9bbb2ac100 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -5,7 +5,9 @@ import {AppBskyEmbedImages} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {isWeb} from 'platform/detection' +import {isWeb} from '#/platform/detection' +import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' +import {atoms as a} from '#/alf' type EventFunction = (index: number) => void @@ -27,20 +29,21 @@ export const GalleryItem: FC = ({ onLongPress, }) => { const {_} = useLingui() + const largeAltBadge = useLargeAltBadgeEnabled() const image = images[index] return ( - + onPress(index) : undefined} onPressIn={onPressIn ? () => onPressIn(index) : undefined} onLongPress={onLongPress ? () => onLongPress(index) : undefined} - style={styles.fullWidth} + style={a.flex_1} accessibilityRole="button" accessibilityLabel={image.alt || _(msg`Image`)} accessibilityHint=""> = ({ {image.alt === '' ? null : ( - + ALT @@ -59,13 +64,6 @@ export const GalleryItem: FC = ({ } const styles = StyleSheet.create({ - fullWidth: { - flex: 1, - }, - image: { - flex: 1, - borderRadius: 4, - }, altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', borderRadius: 6, diff --git a/src/view/com/util/images/ImageHorzList.tsx b/src/view/com/util/images/ImageHorzList.tsx index 12eef14f73..bade2a4446 100644 --- a/src/view/com/util/images/ImageHorzList.tsx +++ b/src/view/com/util/images/ImageHorzList.tsx @@ -2,39 +2,60 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {Image} from 'expo-image' import {AppBskyEmbedImages} from '@atproto/api' +import {Trans} from '@lingui/macro' + +import {atoms as a} from '#/alf' +import {Text} from '#/components/Typography' interface Props { images: AppBskyEmbedImages.ViewImage[] style?: StyleProp + gif?: boolean } -export function ImageHorzList({images, style}: Props) { +export function ImageHorzList({images, style, gif}: Props) { return ( - + {images.map(({thumb, alt}) => ( - + style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}> + + {gif && ( + + + GIF + + + )} + ))} ) } const styles = StyleSheet.create({ - flexRow: { - flexDirection: 'row', - gap: 5, + altContainer: { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + borderRadius: 6, + paddingHorizontal: 6, + paddingVertical: 3, + position: 'absolute', + right: 5, + bottom: 5, + zIndex: 2, }, - image: { - maxWidth: 100, - aspectRatio: 1, - flex: 1, - borderRadius: 4, + alt: { + color: 'white', + fontSize: 7, + fontWeight: 'bold', }, }) diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index c0e743db48..472ce4043a 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -15,7 +15,7 @@ import { import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {HITSLOP_10, HITSLOP_20} from '#/lib/constants' +import {POST_CTRL_HITSLOP} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {makeProfileLink} from '#/lib/routes/links' import {shareUrl} from '#/lib/sharing' @@ -39,6 +39,7 @@ import { } from '#/components/icons/Heart2' import * as Prompt from '#/components/Prompt' import {PostDropdownBtn} from '../forms/PostDropdownBtn' +import {formatCount} from '../numeric/format' import {Text} from '../text/Text' import {RepostButton} from './RepostButton' @@ -214,7 +215,7 @@ let PostCtrls = ({ other: 'Reply (# replies)', })} accessibilityHint="" - hitSlop={big ? HITSLOP_20 : HITSLOP_10}> + hitSlop={POST_CTRL_HITSLOP}> - {post.replyCount} + {formatCount(post.replyCount)} ) : undefined} @@ -257,7 +258,7 @@ let PostCtrls = ({ }) } accessibilityHint="" - hitSlop={big ? HITSLOP_20 : HITSLOP_10}> + hitSlop={POST_CTRL_HITSLOP}> {post.viewer?.like ? ( ) : ( @@ -278,7 +279,7 @@ let PostCtrls = ({ : defaultCtrlColor, ], ]}> - {post.likeCount} + {formatCount(post.likeCount)} ) : undefined} @@ -298,7 +299,7 @@ let PostCtrls = ({ }} accessibilityLabel={_(msg`Share`)} accessibilityHint="" - hitSlop={big ? HITSLOP_20 : HITSLOP_10}> + hitSlop={POST_CTRL_HITSLOP}> diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 81e89d42d9..d49cda442c 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {HITSLOP_10, HITSLOP_20} from '#/lib/constants' +import {POST_CTRL_HITSLOP} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {useRequireAuth} from '#/state/session' import {atoms as a, useTheme} from '#/alf' @@ -12,6 +12,7 @@ import * as Dialog from '#/components/Dialog' import {CloseQuote_Stroke2_Corner1_Rounded as Quote} from '#/components/icons/Quote' import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' import {Text} from '#/components/Typography' +import {formatCount} from '../numeric/format' interface Props { isReposted: boolean @@ -66,7 +67,7 @@ let RepostButton = ({ shape="round" variant="ghost" color="secondary" - hitSlop={big ? HITSLOP_20 : HITSLOP_10}> + hitSlop={POST_CTRL_HITSLOP}> {typeof repostCount !== 'undefined' && repostCount > 0 ? ( - {repostCount} + {formatCount(repostCount)} ) : undefined} diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx index 0898981419..17ab736ced 100644 --- a/src/view/com/util/post-ctrls/RepostButton.web.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.web.tsx @@ -12,6 +12,7 @@ import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repos import * as Menu from '#/components/Menu' import {Text} from '#/components/Typography' import {EventStopper} from '../EventStopper' +import {formatCount} from '../numeric/format' interface Props { isReposted: boolean @@ -115,20 +116,22 @@ const RepostInner = ({ color: {color: string} repostCount?: number big?: boolean -}) => ( - - - {typeof repostCount !== 'undefined' && repostCount > 0 ? ( - - {repostCount} - - ) : undefined} - -) +}) => { + return ( + + + {typeof repostCount !== 'undefined' && repostCount > 0 ? ( + + {formatCount(repostCount)} + + ) : undefined} + + ) +} diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/view/com/util/post-embeds/GifEmbed.tsx index f2e2a8b0e9..1558b75c62 100644 --- a/src/view/com/util/post-embeds/GifEmbed.tsx +++ b/src/view/com/util/post-embeds/GifEmbed.tsx @@ -8,6 +8,7 @@ import {useLingui} from '@lingui/react' import {HITSLOP_20} from '#/lib/constants' import {parseAltFromGIFDescription} from '#/lib/gif-alt-text' import {isWeb} from '#/platform/detection' +import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {EmbedPlayerParams} from 'lib/strings/embed-player' import {useAutoplayDisabled} from 'state/preferences' import {atoms as a, useTheme} from '#/alf' @@ -157,6 +158,7 @@ export function GifEmbed({ function AltText({text}: {text: string}) { const control = Prompt.usePromptControl() + const largeAltBadge = useLargeAltBadgeEnabled() const {_} = useLingui() return ( @@ -169,7 +171,9 @@ function AltText({text}: {text: string}) { hitSlop={HITSLOP_20} onPress={control.open} style={styles.altContainer}> - + ALT diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index a13fffc370..be34a2869e 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -21,6 +21,7 @@ import { import {ImagesLightbox, useLightboxControls} from '#/state/lightbox' import {usePalette} from 'lib/hooks/usePalette' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' +import {atoms as a} from '#/alf' import {ContentHider} from '../../../../components/moderation/ContentHider' import {AutoSizedImage} from '../images/AutoSizedImage' import {ImageLayoutGrid} from '../images/ImageLayoutGrid' @@ -28,6 +29,7 @@ import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {ListEmbed} from './ListEmbed' import {MaybeQuoteEmbed} from './QuoteEmbed' import hairlineWidth = StyleSheet.hairlineWidth +import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' type Embed = | AppBskyEmbedRecord.View @@ -51,6 +53,7 @@ export function PostEmbeds({ }) { const pal = usePalette('default') const {openLightbox} = useLightboxControls() + const largeAltBadge = useLargeAltBadgeEnabled() // quote post with media // = @@ -130,10 +133,12 @@ export function PostEmbeds({ dimensionsHint={aspectRatio} onPress={() => _openLightbox(0)} onPressIn={() => onPressIn(0)} - style={[styles.singleImage]}> + style={a.rounded_sm}> {alt === '' ? null : ( - + ALT @@ -151,9 +156,6 @@ export function PostEmbeds({ images={embed.images} onPress={_openLightbox} onPressIn={onPressIn} - style={ - embed.images.length === 1 ? [styles.singleImage] : undefined - } /> @@ -179,9 +181,6 @@ const styles = StyleSheet.create({ imagesContainer: { marginTop: 8, }, - singleImage: { - borderRadius: 8, - }, altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', borderRadius: 6, diff --git a/src/view/screens/AccessibilitySettings.tsx b/src/view/screens/AccessibilitySettings.tsx index ac0d985f10..9ac9793367 100644 --- a/src/view/screens/AccessibilitySettings.tsx +++ b/src/view/screens/AccessibilitySettings.tsx @@ -4,13 +4,12 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' +import {useAnalytics} from '#/lib/analytics/analytics' +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' +import {s} from '#/lib/styles' import {isNative} from '#/platform/detection' -import {useSetMinimalShellMode} from '#/state/shell' -import {useAnalytics} from 'lib/analytics/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' -import {s} from 'lib/styles' import { useAutoplayDisabled, useHapticsDisabled, @@ -18,11 +17,16 @@ import { useSetAutoplayDisabled, useSetHapticsDisabled, useSetRequireAltTextEnabled, -} from 'state/preferences' -import {ToggleButton} from 'view/com/util/forms/ToggleButton' -import {SimpleViewHeader} from '../com/util/SimpleViewHeader' -import {Text} from '../com/util/text/Text' -import {ScrollView} from '../com/util/Views' +} from '#/state/preferences' +import { + useLargeAltBadgeEnabled, + useSetLargeAltBadgeEnabled, +} from '#/state/preferences/large-alt-badge' +import {useSetMinimalShellMode} from '#/state/shell' +import {ToggleButton} from '#/view/com/util/forms/ToggleButton' +import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' +import {Text} from '#/view/com/util/text/Text' +import {ScrollView} from '#/view/com/util/Views' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -41,6 +45,8 @@ export function AccessibilitySettingsScreen({}: Props) { const setAutoplayDisabled = useSetAutoplayDisabled() const hapticsDisabled = useHapticsDisabled() const setHapticsDisabled = useSetHapticsDisabled() + const largeAltBadgeEnabled = useLargeAltBadgeEnabled() + const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled() useFocusEffect( React.useCallback(() => { @@ -84,6 +90,13 @@ export function AccessibilitySettingsScreen({}: Props) { isSelected={requireAltTextEnabled} onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)} /> + setLargeAltBadgeEnabled(!largeAltBadgeEnabled)} + /> Media diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index f272b90a03..30f8dbebe3 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useMemo} from 'react' import {Pressable, StyleSheet, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useIsFocused, useNavigation} from '@react-navigation/native' @@ -54,7 +55,6 @@ import {atoms as a, useTheme} from '#/alf' import {Button as NewButton, ButtonText} from '#/components/Button' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' import { Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, Heart2_Stroke2_Corner0_Rounded as HeartOutline, @@ -303,15 +303,16 @@ export function ProfileFeedScreenInner({ a.align_center, a.rounded_full, {height: 36, width: 36}, - t.atoms.bg_contrast_50, + t.atoms.bg_contrast_25, (state.hovered || state.pressed) && [ - t.atoms.bg_contrast_100, + t.atoms.bg_contrast_50, ], ]} testID="headerDropdownBtn"> - ) diff --git a/yarn.lock b/yarn.lock index 67ff046cb5..a0fd8749a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,10 +34,10 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@atproto/api@^0.12.19": - version "0.12.19" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.19.tgz#6d842269b6b9cd3fc5864e12824d4fb04cc033cf" - integrity sha512-dsiTpjqBhjGwNW/qG/tLSgUQnmOSvd8hsQr5d8GCUDGK2AEHWl0KNgLPbwxIBEIo8Jg9NHsvqV7BMoix8YreIg== +"@atproto/api@^0.12.20": + version "0.12.20" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.20.tgz#2cada08c24bc61eb1775ee4c8010c7ed9dc5d6f3" + integrity sha512-nt7ZKUQL9j2yQ3tmCCueiIuc0FwdxZYn2fXdLYqltuxlaO5DmaqqULMBKeYJLq4GbvVl/G+ikPJccoSaMWDYOg== dependencies: "@atproto/common-web" "^0.3.0" "@atproto/lexicon" "^0.4.0" @@ -3897,18 +3897,18 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== -"@formatjs/ecma402-abstract@1.18.0": - version "1.18.0" - resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.0.tgz#e2120e7101020140661b58430a7ff4262705a2f2" - integrity sha512-PEVLoa3zBevWSCZzPIM/lvPCi8P5l4G+NXQMc/CjEiaCWgyHieUoo0nM7Bs0n/NbuQ6JpXEolivQ9pKSBHaDlA== +"@formatjs/ecma402-abstract@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz#39197ab90b1c78b7342b129a56a7acdb8f512e17" + integrity sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g== dependencies: - "@formatjs/intl-localematcher" "0.5.2" + "@formatjs/intl-localematcher" "0.5.4" tslib "^2.4.0" -"@formatjs/intl-enumerator@1.4.3": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.4.3.tgz#8d278c273485d7c6219916509fbd51ce3142064d" - integrity sha512-0NpTmAQnDokPoB5aVtXvOdtrUq/uEuPPhBUAr57TYYDjI5MwfFXt8F6JCm6s6CPI0inL8+nxPLjjqH0qyNnP4Q== +"@formatjs/intl-enumerator@1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.4.7.tgz#6ab697f3f8f18cf0cc6a6b028cb9c40db6001f3d" + integrity sha512-03RHnFqfpB4H/jwCwlzC+wkTDk2Fi24JmVIY2PVGvTUpikN2bSr9+8oTXfOC+y7B7VxjCArUnqWXVoctkmy85w== dependencies: tslib "^2.4.0" @@ -3919,30 +3919,39 @@ dependencies: tslib "^2.4.0" -"@formatjs/intl-locale@^3.4.3": - version "3.4.3" - resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-3.4.3.tgz#fdd2a3978b03aa76965abbca86526bb1d02973b6" - integrity sha512-g/35yMikkkRmLYmqE4W74gvZyKa768oC9OmUFzfLmH3CVYF3v2kvAZI0WsxWLbxYj8TT7wBDeLIL3aIlRw4Osw== +"@formatjs/intl-locale@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-4.0.0.tgz#c111a33078413eba2011e82140466261eb1d67cd" + integrity sha512-+4dbMEGsp1bvB3JB3UHH6YTjMnFTifnfdaHp4ROrCCu50NedA69RBsDCG3eivcZkbj57X9ehGhMWjLxlP+gyVw== dependencies: - "@formatjs/ecma402-abstract" "1.18.0" - "@formatjs/intl-enumerator" "1.4.3" + "@formatjs/ecma402-abstract" "2.0.0" + "@formatjs/intl-enumerator" "1.4.7" "@formatjs/intl-getcanonicallocales" "2.3.0" tslib "^2.4.0" -"@formatjs/intl-localematcher@0.5.2": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.2.tgz#5fcf029fd218905575e5080fa33facdcb623d532" - integrity sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw== +"@formatjs/intl-localematcher@0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz#caa71f2e40d93e37d58be35cfffe57865f2b366f" + integrity sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g== dependencies: tslib "^2.4.0" -"@formatjs/intl-pluralrules@^5.2.10": - version "5.2.10" - resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.10.tgz#379fc06133625df0cae715c1d902001974ff3279" - integrity sha512-wfJypePrbOByaZVPP1moLXHgS9LeAvi9coP95XZX7ySVrwdDGPnxz9Pw+o7J1o8AjLxjiqGrvAi74key5zzIjQ== +"@formatjs/intl-numberformat@^8.10.3": + version "8.10.3" + resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-8.10.3.tgz#abc97cc6a7b7f1b20da9f07a976b5589c1192ab8" + integrity sha512-lH3liLMeIjZ19Zxt8RRPnBcpPweS1YNSXRURDiFfvFmRlDZUOd8+GlcVyECcPZPkIoSH/p4lfGrnaUzepxJ92g== + dependencies: + "@formatjs/ecma402-abstract" "2.0.0" + "@formatjs/intl-localematcher" "0.5.4" + tslib "^2.4.0" + +"@formatjs/intl-pluralrules@^5.2.14": + version "5.2.14" + resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.14.tgz#7477bd2aa9bfde9e543d839707eff5460eb08026" + integrity sha512-l6Ev7aOGXJSh5EPDEqzsbyufdCCKXZk993QXRQebLsB0TXRhIyF4alqjdMEatLwIigK/Mka8kiVIOLeFP5Cj9Q== dependencies: - "@formatjs/ecma402-abstract" "1.18.0" - "@formatjs/intl-localematcher" "0.5.2" + "@formatjs/ecma402-abstract" "2.0.0" + "@formatjs/intl-localematcher" "0.5.4" tslib "^2.4.0" "@fortawesome/fontawesome-common-types@6.4.2":