diff --git a/.github/workflows/build-and-push-embedr-aws.yaml b/.github/workflows/build-and-push-embedr-aws.yaml new file mode 100644 index 0000000000..f7f24af9f7 --- /dev/null +++ b/.github/workflows/build-and-push-embedr-aws.yaml @@ -0,0 +1,57 @@ +name: build-and-push-embedr-aws +on: + push: + branches: + - main + - bnewbold/embedr + - bnewbold/embedr-rebase + +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: embed + +jobs: + embedr-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.embedr + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 3ad05b6ec6..568cbf7b41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,3 +72,5 @@ CMD ["/usr/bin/bskyweb"] LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app LABEL org.opencontainers.image.description="bsky.app Web App" LABEL org.opencontainers.image.licenses=MIT + +# NOOP diff --git a/Dockerfile.embedr b/Dockerfile.embedr new file mode 100644 index 0000000000..c70251658b --- /dev/null +++ b/Dockerfile.embedr @@ -0,0 +1,78 @@ +FROM golang:1.21-bullseye AS build-env + +WORKDIR /usr/src/social-app + +ENV DEBIAN_FRONTEND=noninteractive + +# Node +ENV NODE_VERSION=18 +ENV NVM_DIR=/usr/share/nvm + +# Go +ENV GODEBUG="netdns=go" +ENV GOOS="linux" +ENV GOARCH="amd64" +ENV CGO_ENABLED=1 +ENV GOEXPERIMENT="loopvar" + +COPY . . + +# +# Generate the JavaScript webpack. NOTE: this will change +# +RUN mkdir --parents $NVM_DIR && \ + wget \ + --output-document=/tmp/nvm-install.sh \ + https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh && \ + bash /tmp/nvm-install.sh + +RUN \. "$NVM_DIR/nvm.sh" && \ + nvm install $NODE_VERSION && \ + nvm use $NODE_VERSION && \ + npm install --global yarn && \ + yarn && \ + cd bskyembed && yarn install --frozen-lockfile && cd .. && \ + yarn intl:build && \ + yarn build-embed + +# DEBUG +RUN find ./bskyweb/embedr-static && find ./bskyweb/embedr-templates && find ./bskyembed/dist + +# hack around issue with empty directory and go:embed +RUN touch bskyweb/static/js/empty.txt + +# +# Generate the embedr Go binary. +# +RUN cd bskyweb/ && \ + go mod download && \ + go mod verify + +RUN cd bskyweb/ && \ + go build \ + -v \ + -trimpath \ + -tags timetzdata \ + -o /embedr \ + ./cmd/embedr + +FROM debian:bullseye-slim + +ENV GODEBUG=netdns=go +ENV TZ=Etc/UTC +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install --yes \ + dumb-init \ + ca-certificates + +ENTRYPOINT ["dumb-init", "--"] + +WORKDIR /embedr +COPY --from=build-env /embedr /usr/bin/embedr + +CMD ["/usr/bin/embedr"] + +LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app +LABEL org.opencontainers.image.description="embed.bsky.app Web App" +LABEL org.opencontainers.image.licenses=MIT diff --git a/Makefile b/Makefile index c90abb783e..a40d37610e 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,11 @@ build-web: ## Compile web bundle, copy to bskyweb directory yarn intl:build yarn build-web +.PHONY: build-web-embed +build-web-embed: ## Compile web embed bundle, copy to bskyweb/embedr* directories + yarn intl:build + yarn build-embed + .PHONY: test test: ## Run all tests NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test @@ -28,6 +33,7 @@ lint: ## Run style checks and verify syntax .PHONY: deps deps: ## Installs dependent libs using 'yarn install' yarn install --frozen-lockfile + cd bskyembed && yarn install --frozen-lockfile .PHONY: nvm-setup nvm-setup: ## Use NVM to install and activate node+yarn diff --git a/__e2e__/tests/composer.test.ts b/__e2e__/tests/composer.test.ts index 34f8ae39ec..06781410f6 100644 --- a/__e2e__/tests/composer.test.ts +++ b/__e2e__/tests/composer.test.ts @@ -1,8 +1,9 @@ /* eslint-env detox/detox */ -import {describe, beforeAll, it} from '@jest/globals' +import {beforeAll, describe, it} from '@jest/globals' import {expect} from 'detox' -import {openApp, loginAsAlice, createServer, sleep} from '../util' + +import {createServer, loginAsAlice, openApp, sleep} from '../util' describe('Composer', () => { beforeAll(async () => { @@ -41,7 +42,6 @@ describe('Composer', () => { await element(by.id('composerTextInput')).typeText( 'Post with a https://example.com link card', ) - await element(by.id('addLinkCardBtn')).tap() await element(by.id('composerPublishBtn')).tap() await expect(element(by.id('composeFAB'))).toBeVisible() }) @@ -72,7 +72,6 @@ describe('Composer', () => { await element(by.id('composerTextInput')).typeText( 'Reply with a https://example.com link card', ) - await element(by.id('addLinkCardBtn')).tap() await element(by.id('composerPublishBtn')).tap() await expect(element(by.id('composeFAB'))).toBeVisible() }) @@ -104,7 +103,6 @@ describe('Composer', () => { await element(by.id('composerTextInput')).typeText( 'QP with a https://example.com link card', ) - await element(by.id('addLinkCardBtn')).tap() await element(by.id('composerPublishBtn')).tap() await expect(element(by.id('composeFAB'))).toBeVisible() }) diff --git a/__e2e__/util.ts b/__e2e__/util.ts index a7869a2e1d..70fbdb6010 100644 --- a/__e2e__/util.ts +++ b/__e2e__/util.ts @@ -1,5 +1,5 @@ -import {resolveConfig} from 'detox/internals' import {execSync} from 'child_process' +import {resolveConfig} from 'detox/internals' import http from 'http' const platform = device.getPlatform() @@ -52,7 +52,7 @@ export async function login( if (await isVisible('chooseAccountForm')) { await element(by.id('chooseNewAccountBtn')).tap() } - await element(by.id('loginSelectServiceButton')).tap() + await element(by.id('selectServiceButton')).tap() if (takeScreenshots) { await device.takeScreenshot('2- opened service selector') } diff --git a/__tests__/lib/images.test.ts b/__tests__/lib/images.test.ts index 38b722e2ce..595f566c47 100644 --- a/__tests__/lib/images.test.ts +++ b/__tests__/lib/images.test.ts @@ -1,9 +1,10 @@ +import ImageResizer from '@bam.tech/react-native-image-resizer' +import RNFetchBlob from 'rn-fetch-blob' + import { downloadAndResize, DownloadAndResizeOpts, } from '../../src/lib/media/manip' -import ImageResizer from '@bam.tech/react-native-image-resizer' -import RNFetchBlob from 'rn-fetch-blob' describe('downloadAndResize', () => { const errorSpy = jest.spyOn(global.console, 'error') @@ -30,6 +31,7 @@ describe('downloadAndResize', () => { const mockedFetch = RNFetchBlob.fetch as jest.Mock mockedFetch.mockResolvedValueOnce({ path: jest.fn().mockReturnValue('file://downloaded-image.jpg'), + info: jest.fn().mockReturnValue({status: 200}), flush: jest.fn(), }) @@ -84,6 +86,7 @@ describe('downloadAndResize', () => { const mockedFetch = RNFetchBlob.fetch as jest.Mock mockedFetch.mockResolvedValueOnce({ path: jest.fn().mockReturnValue('file://downloaded-image'), + info: jest.fn().mockReturnValue({status: 200}), flush: jest.fn(), }) @@ -118,4 +121,26 @@ describe('downloadAndResize', () => { {mode: 'cover'}, ) }) + + it('should return undefined for non-200 response', async () => { + const mockedFetch = RNFetchBlob.fetch as jest.Mock + mockedFetch.mockResolvedValueOnce({ + path: jest.fn().mockReturnValue('file://downloaded-image'), + info: jest.fn().mockReturnValue({status: 400}), + flush: jest.fn(), + }) + + const opts: DownloadAndResizeOpts = { + uri: 'https://example.com/image', + width: 100, + height: 100, + maxSize: 500000, + mode: 'cover', + timeout: 10000, + } + + const result = await downloadAndResize(opts) + expect(errorSpy).not.toHaveBeenCalled() + expect(result).toBeUndefined() + }) }) diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index f003e5acc0..2f603a521d 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -1,17 +1,18 @@ import {RichText} from '@atproto/api' + +import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' +import {cleanError} from '../../src/lib/strings/errors' +import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles' +import {enforceLen, pluralize} from '../../src/lib/strings/helpers' +import {detectLinkables} from '../../src/lib/strings/rich-text-detection' +import {shortenLinks} from '../../src/lib/strings/rich-text-manip' +import {ago} from '../../src/lib/strings/time' import { makeRecordUri, toNiceDomain, - toShortUrl, toShareUrl, + toShortUrl, } from '../../src/lib/strings/url-helpers' -import {pluralize, enforceLen} from '../../src/lib/strings/helpers' -import {ago} from '../../src/lib/strings/time' -import {detectLinkables} from '../../src/lib/strings/rich-text-detection' -import {shortenLinks} from '../../src/lib/strings/rich-text-manip' -import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles' -import {cleanError} from '../../src/lib/strings/errors' -import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' describe('detectLinkables', () => { const inputs = [ @@ -395,6 +396,7 @@ describe('parseEmbedPlayerFromUrl', () => { 'https://youtube.com/watch?v=videoId&feature=share', 'https://youtube.com/shorts/videoId', 'https://m.youtube.com/watch?v=videoId', + 'https://music.youtube.com/watch?v=videoId', 'https://youtube.com/shorts/', 'https://youtube.com/', @@ -434,6 +436,8 @@ describe('parseEmbedPlayerFromUrl', () => { 'https://giphy.com/gif/some-random-gif-name-gifId', 'https://giphy.com/gifs/', + 'https://giphy.com/gifs/39248209509382934029?hh=100&ww=100', + 'https://media.giphy.com/media/gifId/giphy.webp', 'https://media0.giphy.com/media/gifId/giphy.webp', 'https://media1.giphy.com/media/gifId/giphy.gif', @@ -456,6 +460,12 @@ describe('parseEmbedPlayerFromUrl', () => { 'https://tenor.com/view', 'https://tenor.com/view/gifId.gif', 'https://tenor.com/intl/view/gifId.gif', + + 'https://media.tenor.com/someID_AAAAC/someName.gif?hh=100&ww=100', + 'https://media.tenor.com/someID_AAAAC/someName.gif', + 'https://media.tenor.com/someID/someName.gif', + 'https://media.tenor.com/someID', + 'https://media.tenor.com', ] const outputs = [ @@ -495,6 +505,11 @@ describe('parseEmbedPlayerFromUrl', () => { source: 'youtube', playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0', }, + { + type: 'youtube_video', + source: 'youtube', + playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0', + }, undefined, undefined, @@ -621,18 +636,25 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, undefined, undefined, - + { + type: 'giphy_gif', + source: 'giphy', + isGif: true, + hideDetails: true, + metaUri: 'https://giphy.com/gifs/39248209509382934029', + playerUri: 'https://i.giphy.com/media/39248209509382934029/200.webp', + }, { type: 'giphy_gif', source: 'giphy', isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, { type: 'giphy_gif', @@ -640,7 +662,7 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, { type: 'giphy_gif', @@ -648,7 +670,7 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, { type: 'giphy_gif', @@ -656,7 +678,7 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, { type: 'giphy_gif', @@ -664,7 +686,7 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, { type: 'giphy_gif', @@ -672,7 +694,7 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, undefined, undefined, @@ -684,7 +706,7 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, { @@ -693,7 +715,7 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, { type: 'giphy_gif', @@ -701,7 +723,7 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, { type: 'giphy_gif', @@ -709,7 +731,7 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, { type: 'giphy_gif', @@ -717,32 +739,30 @@ describe('parseEmbedPlayerFromUrl', () => { isGif: true, hideDetails: true, metaUri: 'https://giphy.com/gifs/gifId', - playerUri: 'https://i.giphy.com/media/gifId/giphy.webp', + playerUri: 'https://i.giphy.com/media/gifId/200.webp', }, - { - type: 'tenor_gif', - source: 'tenor', - isGif: true, - hideDetails: true, - playerUri: 'https://tenor.com/view/gifId.gif', - }, undefined, undefined, + undefined, + undefined, + undefined, + { type: 'tenor_gif', source: 'tenor', isGif: true, hideDetails: true, - playerUri: 'https://tenor.com/view/gifId.gif', - }, - { - type: 'tenor_gif', - source: 'tenor', - isGif: true, - hideDetails: true, - playerUri: 'https://tenor.com/intl/view/gifId.gif', + playerUri: 'https://t.gifs.bsky.app/someID_AAAAM/someName.gif', + dimensions: { + width: 100, + height: 100, + }, }, + undefined, + undefined, + undefined, + undefined, ] it('correctly grabs the correct id from uri', () => { diff --git a/__tests__/lib/strings/url-helpers.test.ts b/__tests__/lib/strings/url-helpers.test.ts index fb4b8f7555..0b1b750281 100644 --- a/__tests__/lib/strings/url-helpers.test.ts +++ b/__tests__/lib/strings/url-helpers.test.ts @@ -1,10 +1,10 @@ -import {it, describe, expect} from '@jest/globals' +import {describe, expect, it} from '@jest/globals' import { - linkRequiresWarning, isPossiblyAUrl, - splitApexDomain, isTrustedUrl, + linkRequiresWarning, + splitApexDomain, } from '../../../src/lib/strings/url-helpers' describe('linkRequiresWarning', () => { @@ -170,6 +170,7 @@ describe('isTrustedUrl', () => { ['https://google.com', false], ['https://docs.google.com', false], ['https://google.com/#', false], + ['https://blueskywebxzendesk.com', false], ] it.each(cases)('given input uri %p, returns %p', (str, expected) => { diff --git a/app.config.js b/app.config.js index 9036d5e331..dbec561952 100644 --- a/app.config.js +++ b/app.config.js @@ -35,15 +35,20 @@ module.exports = function (config) { */ const PLATFORM = process.env.EAS_BUILD_PLATFORM - const DIST_BUILD_NUMBER = - PLATFORM === 'android' - ? process.env.BSKY_ANDROID_VERSION_CODE - : process.env.BSKY_IOS_BUILD_NUMBER - const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development' const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight' + const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production' + + const UPDATES_CHANNEL = IS_TESTFLIGHT + ? 'testflight' + : IS_PRODUCTION + ? 'production' + : undefined + const UPDATES_ENABLED = !!UPDATES_CHANNEL - const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production' + const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${ + IS_DEV ? 'dev' : '' + }` return { expo: { @@ -89,6 +94,11 @@ module.exports = function (config) { barStyle: 'light-content', backgroundColor: '#00000000', }, + // Dark nav bar in light mode is better than light nav bar in dark mode + androidNavigationBar: { + barStyle: 'light-content', + backgroundColor: DARK_SPLASH_CONFIG_ANDROID.backgroundColor, + }, android: { icon: './assets/icon.png', adaptiveIcon: { @@ -126,14 +136,12 @@ module.exports = function (config) { }, updates: { url: 'https://updates.bsky.app/manifest', - // TODO Eventually we want to enable this for all environments, but for now it will only be used for - // TestFlight builds - enabled: IS_TESTFLIGHT, + enabled: UPDATES_ENABLED, fallbackToCacheTimeout: 30000, - codeSigningCertificate: IS_TESTFLIGHT + codeSigningCertificate: UPDATES_ENABLED ? './code-signing/certificate.pem' : undefined, - codeSigningMetadata: IS_TESTFLIGHT + codeSigningMetadata: UPDATES_ENABLED ? { keyid: 'main', alg: 'rsa-v1_5-sha256', @@ -208,7 +216,7 @@ module.exports = function (config) { organization: 'blueskyweb', project: 'react-native', release: VERSION, - dist: `${PLATFORM}.${VERSION}.${DIST_BUILD_NUMBER}`, + dist: SENTRY_DIST, }, }, ], diff --git a/assets/icons/alien_stroke2_corner0_rounded.svg b/assets/icons/alien_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..595308c97b --- /dev/null +++ b/assets/icons/alien_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/apple_stroke2_corner0_rounded.svg b/assets/icons/apple_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..3c7f051a3c --- /dev/null +++ b/assets/icons/apple_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/arrowLeft_stroke2_corner0_rounded.svg b/assets/icons/arrowLeft_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..96b5c16f50 --- /dev/null +++ b/assets/icons/arrowLeft_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/atom_stroke2_corner0_rounded.svg b/assets/icons/atom_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..723fdeab6d --- /dev/null +++ b/assets/icons/atom_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/bubble_filled_stroke2_corner2_rounded.svg b/assets/icons/bubble_filled_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..3dbc8ca61d --- /dev/null +++ b/assets/icons/bubble_filled_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/bubble_stroke2_corner2_rounded.svg b/assets/icons/bubble_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..e0399eda87 --- /dev/null +++ b/assets/icons/bubble_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/celebrate_stroke2_corner0_rounded.svg b/assets/icons/celebrate_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..3ea7bc8d2a --- /dev/null +++ b/assets/icons/celebrate_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/codeBrackets_stroke2_corner0_rounded.svg b/assets/icons/codeBrackets_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..0cc239210e --- /dev/null +++ b/assets/icons/codeBrackets_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/coffee_stroke2_corner0_rounded.svg b/assets/icons/coffee_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..b734ef606a --- /dev/null +++ b/assets/icons/coffee_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/emojiArc_stroke2_corner0_rounded.svg b/assets/icons/emojiArc_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..3d09228e71 --- /dev/null +++ b/assets/icons/emojiArc_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/emojiHeartEyes_stroke2_corner0_rounded.svg b/assets/icons/emojiHeartEyes_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..4aecb86c54 --- /dev/null +++ b/assets/icons/emojiHeartEyes_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/envelope_filled_stroke2_corner0_rounded.svg b/assets/icons/envelope_filled_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..3810bf334f --- /dev/null +++ b/assets/icons/envelope_filled_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/explosion_stroke2_corner0_rounded.svg b/assets/icons/explosion_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..11544bf79c --- /dev/null +++ b/assets/icons/explosion_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/gameController_stroke2_corner0_rounded.svg b/assets/icons/gameController_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..e530c802b0 --- /dev/null +++ b/assets/icons/gameController_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/gifSquare_stroke2_corner0_rounded.svg b/assets/icons/gifSquare_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..47b9df9846 --- /dev/null +++ b/assets/icons/gifSquare_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/gif_stroke2_corner0_rounded.svg b/assets/icons/gif_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..519acfd4d2 --- /dev/null +++ b/assets/icons/gif_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/image_stroke2_corner0_rounded.svg b/assets/icons/image_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..389020b0d1 --- /dev/null +++ b/assets/icons/image_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/lab_stroke2_corner0_rounded.svg b/assets/icons/lab_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..466809194e --- /dev/null +++ b/assets/icons/lab_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/leaf_stroke2_corner0_rounded.svg b/assets/icons/leaf_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..16b379f98c --- /dev/null +++ b/assets/icons/leaf_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/musicNote_stroke2_corner0_rounded.svg b/assets/icons/musicNote_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..2dcc2e3b29 --- /dev/null +++ b/assets/icons/musicNote_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/pencil_stroke2_corner0_rounded.svg b/assets/icons/pencil_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..7341989894 --- /dev/null +++ b/assets/icons/pencil_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/peopleRemove2_stroke2_corner0_rounded.svg b/assets/icons/peopleRemove2_stroke2_corner0_rounded.svg index 2e798cbe29..daec6f5579 100644 --- a/assets/icons/peopleRemove2_stroke2_corner0_rounded.svg +++ b/assets/icons/peopleRemove2_stroke2_corner0_rounded.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/piggyBank_stroke2_corner0_rounded.svg b/assets/icons/piggyBank_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..36d3060102 --- /dev/null +++ b/assets/icons/piggyBank_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/pizza_stroke2_corner0_rounded.svg b/assets/icons/pizza_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..e63351b897 --- /dev/null +++ b/assets/icons/pizza_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/poop_stroke2_corner0_rounded.svg b/assets/icons/poop_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..daa6c11126 --- /dev/null +++ b/assets/icons/poop_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/repost_stroke2_corner2_rounded.svg b/assets/icons/repost_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..c6d84cd5e6 --- /dev/null +++ b/assets/icons/repost_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/rose_stroke2_corner0_rounded.svg b/assets/icons/rose_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..2d269855bd --- /dev/null +++ b/assets/icons/rose_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/settingsSliderVertical_stroke2_corner0_rounded.svg b/assets/icons/settingsSliderVertical_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..518261bca9 --- /dev/null +++ b/assets/icons/settingsSliderVertical_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/shaka_stroke2_corner0_rounded.svg b/assets/icons/shaka_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..32af469e4a --- /dev/null +++ b/assets/icons/shaka_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/timesLarge_stroke2_corner0_rounded.svg b/assets/icons/timesLarge_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..68403f5984 --- /dev/null +++ b/assets/icons/timesLarge_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/ufo_stroke2_corner0_rounded.svg b/assets/icons/ufo_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..115c589e0a --- /dev/null +++ b/assets/icons/ufo_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/zap_stroke2_corner0_rounded.svg b/assets/icons/zap_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..06a88ad8d2 --- /dev/null +++ b/assets/icons/zap_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bskyembed/.eslintrc b/bskyembed/.eslintrc new file mode 100644 index 0000000000..339900dd09 --- /dev/null +++ b/bskyembed/.eslintrc @@ -0,0 +1,20 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint", "simple-import-sort"], + "extends": [ + "eslint:recommended", + "preact", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ], + "rules": { + "simple-import-sort/imports": "warn", + "simple-import-sort/exports": "warn" + }, + "parserOptions": { + "sourceType": "module", + "ecmaVersion": "latest", + "project": "./tsconfig.json" + } +} \ No newline at end of file diff --git a/bskyembed/.gitignore b/bskyembed/.gitignore new file mode 100644 index 0000000000..d451ff16c1 --- /dev/null +++ b/bskyembed/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/bskyembed/assets/arrowBottom_stroke2_corner0_rounded.svg b/bskyembed/assets/arrowBottom_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..afb8f245f7 --- /dev/null +++ b/bskyembed/assets/arrowBottom_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bskyembed/assets/bubble_filled_stroke2_corner2_rounded.svg b/bskyembed/assets/bubble_filled_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..9962a20bdb --- /dev/null +++ b/bskyembed/assets/bubble_filled_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/bskyembed/assets/circleInfo_stroke2_corner0_rounded.svg b/bskyembed/assets/circleInfo_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..6e098673dd --- /dev/null +++ b/bskyembed/assets/circleInfo_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/bskyembed/assets/heart2_filled_stroke2_corner0_rounded.svg b/bskyembed/assets/heart2_filled_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..18d351c7bc --- /dev/null +++ b/bskyembed/assets/heart2_filled_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/bskyembed/assets/logo.svg b/bskyembed/assets/logo.svg new file mode 100644 index 0000000000..f671e1128d --- /dev/null +++ b/bskyembed/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/bskyembed/assets/repost_stroke2_corner2_rounded.svg b/bskyembed/assets/repost_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..a31e504a12 --- /dev/null +++ b/bskyembed/assets/repost_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/bskyembed/index.html b/bskyembed/index.html new file mode 100644 index 0000000000..61d0c7d17a --- /dev/null +++ b/bskyembed/index.html @@ -0,0 +1,19 @@ + + + + + + Bluesky Embed + + + + + + + + + +
+ + + diff --git a/bskyembed/package.json b/bskyembed/package.json new file mode 100644 index 0000000000..f610e8c064 --- /dev/null +++ b/bskyembed/package.json @@ -0,0 +1,29 @@ +{ + "name": "bskyembed", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "build-snippet": "tsc --project tsconfig.snippet.json", + "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src" + }, + "dependencies": { + "@atproto/api": "^0.12.2", + "@preact/preset-vite": "^2.8.2", + "@vitejs/plugin-legacy": "^5.3.2", + "preact": "^10.4.8", + "terser": "^5.30.3" + }, + "devDependencies": { + "autoprefixer": "^10.4.19", + "eslint": "^8.19.0", + "eslint-config-preact": "^1.3.0", + "eslint-plugin-simple-import-sort": "^12.0.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "typescript": "^4.0.5", + "vite": "^5.2.8", + "vite-tsconfig-paths": "^4.3.2" + } +} diff --git a/bskyembed/post.html b/bskyembed/post.html new file mode 100644 index 0000000000..5f550495f2 --- /dev/null +++ b/bskyembed/post.html @@ -0,0 +1,19 @@ + + + + + + Bluesky Embed + + + + + + + + + +
+ + + diff --git a/bskyembed/postcss.config.cjs b/bskyembed/postcss.config.cjs new file mode 100644 index 0000000000..33ad091d26 --- /dev/null +++ b/bskyembed/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/bskyembed/snippet/embed.ts b/bskyembed/snippet/embed.ts new file mode 100644 index 0000000000..380cda5fb9 --- /dev/null +++ b/bskyembed/snippet/embed.ts @@ -0,0 +1,100 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +interface Window { + bluesky: { + scan: (element?: Pick) => void + } +} + +const EMBED_URL = 'https://embed.bsky.app' + +window.bluesky = window.bluesky || { + scan, +} + +/** + * Listen for messages from the Bluesky embed iframe and adjust the height of + * the iframe accordingly. + */ +window.addEventListener('message', event => { + if (event.origin !== EMBED_URL) { + return + } + + const id = (event.data as {id: string}).id + if (!id) { + return + } + + const embed = document.querySelector( + `[data-bluesky-id="${id}"]`, + ) + + if (!embed) { + return + } + + const height = (event.data as {height: number}).height + if (height) { + embed.style.height = `${height}px` + } +}) + +/** + * Scan the document for all elements with the data-bluesky-aturi attribute, + * and initialize them as Bluesky embeds. + * + * @param element Only scan this specific element @default document @optional + * @returns + */ +function scan(node = document) { + const embeds = node.querySelectorAll('[data-bluesky-uri]') + + for (let i = 0; i < embeds.length; i++) { + const id = String(Math.random()).slice(2) + + const embed = embeds[i] + const aturi = embed.getAttribute('data-bluesky-uri') + + if (!aturi) { + continue + } + + const ref_url = location.origin + location.pathname + + const searchParams = new URLSearchParams() + searchParams.set('id', id) + if (ref_url.startsWith('http')) { + searchParams.set('ref_url', encodeURIComponent(ref_url)) + } + + const iframe = document.createElement('iframe') + iframe.setAttribute('data-bluesky-id', id) + iframe.src = `${EMBED_URL}/embed/${aturi.slice( + 'at://'.length, + )}?${searchParams.toString()}` + iframe.width = '100%' + iframe.style.border = 'none' + iframe.style.display = 'block' + iframe.style.flexGrow = '1' + iframe.frameBorder = '0' + iframe.scrolling = 'no' + + const container = document.createElement('div') + container.style.maxWidth = '600px' + container.style.width = '100%' + container.style.marginTop = '10px' + container.style.marginBottom = '10px' + container.style.display = 'flex' + container.className = 'bluesky-embed' + + container.appendChild(iframe) + + embed.replaceWith(container) + } +} + +if (['interactive', 'complete'].indexOf(document.readyState) !== -1) { + scan() +} else { + document.addEventListener('DOMContentLoaded', () => scan()) +} diff --git a/bskyembed/src/components/container.tsx b/bskyembed/src/components/container.tsx new file mode 100644 index 0000000000..5b1b2b7fb4 --- /dev/null +++ b/bskyembed/src/components/container.tsx @@ -0,0 +1,55 @@ +import {ComponentChildren, h} from 'preact' +import {useEffect, useRef} from 'preact/hooks' + +import {Link} from './link' + +export function Container({ + children, + href, +}: { + children: ComponentChildren + href?: string +}) { + const ref = useRef(null) + const prevHeight = useRef(0) + + useEffect(() => { + if (ref.current) { + const observer = new ResizeObserver(entries => { + const entry = entries[0] + if (!entry) return + + let {height} = entry.contentRect + height += 2 // border top and bottom + if (height !== prevHeight.current) { + prevHeight.current = height + window.parent.postMessage( + {height, id: new URLSearchParams(window.location.search).get('id')}, + '*', + ) + } + }) + observer.observe(ref.current) + return () => observer.disconnect() + } + }, []) + + return ( +
{ + if (ref.current && href) { + // forwardRef requires preact/compat - let's keep it simple + // to keep the bundle size down + const anchor = ref.current.querySelector('a') + if (anchor) { + anchor.click() + } + } + }}> + {href && } +
{children}
+
+ ) +} diff --git a/bskyembed/src/components/embed.tsx b/bskyembed/src/components/embed.tsx new file mode 100644 index 0000000000..4457defce4 --- /dev/null +++ b/bskyembed/src/components/embed.tsx @@ -0,0 +1,351 @@ +import { + AppBskyEmbedExternal, + AppBskyEmbedImages, + AppBskyEmbedRecord, + AppBskyEmbedRecordWithMedia, + AppBskyFeedDefs, + AppBskyFeedPost, + AppBskyGraphDefs, + AppBskyLabelerDefs, +} from '@atproto/api' +import {ComponentChildren, h} from 'preact' +import {useMemo} from 'preact/hooks' + +import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg' +import {CONTENT_LABELS, labelsToInfo} from '../labels' +import {getRkey} from '../utils' +import {Link} from './link' + +export function Embed({ + content, + labels, + hideRecord, +}: { + content: AppBskyFeedDefs.PostView['embed'] + labels: AppBskyFeedDefs.PostView['labels'] + hideRecord?: boolean +}) { + const labelInfo = useMemo(() => labelsToInfo(labels), [labels]) + + if (!content) return null + + try { + // Case 1: Image + if (AppBskyEmbedImages.isView(content)) { + return + } + + // Case 2: External link + if (AppBskyEmbedExternal.isView(content)) { + return + } + + // Case 3: Record (quote or linked post) + if (AppBskyEmbedRecord.isView(content)) { + if (hideRecord) { + return null + } + + const record = content.record + + // Case 3.1: Post + if (AppBskyEmbedRecord.isViewRecord(record)) { + const pwiOptOut = !!record.author.labels?.find( + label => label.val === '!no-unauthenticated', + ) + if (pwiOptOut) { + return ( + + The author of the quoted post has requested their posts not be + displayed on external sites. + + ) + } + + let text + if (AppBskyFeedPost.isRecord(record.value)) { + text = record.value.text + } + + const isAuthorLabeled = record.author.labels?.some(label => + CONTENT_LABELS.includes(label.val), + ) + + return ( + +
+
+ +
+

+ {record.author.displayName} + + @{record.author.handle} + +

+
+ {text &&

{text}

} + {record.embeds?.map(embed => ( + + ))} + + ) + } + + // Case 3.2: List + if (AppBskyGraphDefs.isListView(record)) { + return ( + + ) + } + + // Case 3.3: Feed + if (AppBskyFeedDefs.isGeneratorView(record)) { + return ( + + ) + } + + // Case 3.4: Labeler + if (AppBskyLabelerDefs.isLabelerView(record)) { + return ( + + ) + } + + // Case 3.5: Post not found + if (AppBskyEmbedRecord.isViewNotFound(record)) { + return Quoted post not found, it may have been deleted. + } + + // Case 3.6: Post blocked + if (AppBskyEmbedRecord.isViewBlocked(record)) { + return The quoted post is blocked. + } + + throw new Error('Unknown embed type') + } + + // Case 4: Record with media + if ( + AppBskyEmbedRecordWithMedia.isView(content) && + AppBskyEmbedRecord.isViewRecord(content.record.record) + ) { + return ( +
+ + +
+ ) + } + + throw new Error('Unsupported embed type') + } catch (err) { + return ( + {err instanceof Error ? err.message : 'An error occurred'} + ) + } +} + +function Info({children}: {children: ComponentChildren}) { + return ( +
+ +

{children}

+
+ ) +} + +function ImageEmbed({ + content, + labelInfo, +}: { + content: AppBskyEmbedImages.View + labelInfo?: string +}) { + if (labelInfo) { + return {labelInfo} + } + + switch (content.images.length) { + case 1: + return ( + {content.images[0].alt} + ) + case 2: + return ( +
+ {content.images.map((image, i) => ( + {image.alt} + ))} +
+ ) + case 3: + return ( +
+ {content.images[0].alt} +
+ {content.images.slice(1).map((image, i) => ( + {image.alt} + ))} +
+
+ ) + case 4: + return ( +
+ {content.images.map((image, i) => ( + {image.alt} + ))} +
+ ) + default: + return null + } +} + +function ExternalEmbed({ + content, + labelInfo, +}: { + content: AppBskyEmbedExternal.View + labelInfo?: string +}) { + function toNiceDomain(url: string): string { + try { + const urlp = new URL(url) + return urlp.host ? urlp.host : url + } catch (e) { + return url + } + } + + if (labelInfo) { + return {labelInfo} + } + + return ( + + {content.external.thumb && ( + + )} +
+

+ {toNiceDomain(content.external.uri)} +

+

{content.external.title}

+

+ {content.external.description} +

+
+ + ) +} + +function GenericWithImage({ + title, + subtitle, + href, + image, + description, +}: { + title: string + subtitle: string + href: string + image?: string + description?: string +}) { + return ( + +
+ {image ? ( + {title} + ) : ( +
+ )} +
+

{title}

+

{subtitle}

+
+
+ {description &&

{description}

} + + ) +} diff --git a/bskyembed/src/components/link.tsx b/bskyembed/src/components/link.tsx new file mode 100644 index 0000000000..64c2c9a83b --- /dev/null +++ b/bskyembed/src/components/link.tsx @@ -0,0 +1,32 @@ +import {h} from 'preact' + +export function Link({ + href, + className, + ...props +}: { + href: string + className?: string +} & h.JSX.HTMLAttributes) { + const searchParam = new URLSearchParams(window.location.search) + const ref_url = searchParam.get('ref_url') + + const newSearchParam = new URLSearchParams() + newSearchParam.set('ref_src', 'embed') + if (ref_url) { + newSearchParam.set('ref_url', ref_url) + } + + return ( + evt.stopPropagation()} + className={`cursor-pointer ${className || ''}`} + {...props} + /> + ) +} diff --git a/bskyembed/src/components/post.tsx b/bskyembed/src/components/post.tsx new file mode 100644 index 0000000000..3f2c745bdd --- /dev/null +++ b/bskyembed/src/components/post.tsx @@ -0,0 +1,160 @@ +import {AppBskyFeedDefs, AppBskyFeedPost, RichText} from '@atproto/api' +import {h} from 'preact' + +import replyIcon from '../../assets/bubble_filled_stroke2_corner2_rounded.svg' +import likeIcon from '../../assets/heart2_filled_stroke2_corner0_rounded.svg' +import logo from '../../assets/logo.svg' +import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg' +import {CONTENT_LABELS} from '../labels' +import {getRkey, niceDate} from '../utils' +import {Container} from './container' +import {Embed} from './embed' +import {Link} from './link' + +interface Props { + thread: AppBskyFeedDefs.ThreadViewPost +} + +export function Post({thread}: Props) { + const post = thread.post + + const isAuthorLabeled = post.author.labels?.some(label => + CONTENT_LABELS.includes(label.val), + ) + + let record: AppBskyFeedPost.Record | null = null + if (AppBskyFeedPost.isRecord(post.record)) { + record = post.record + } + + const href = `/profile/${post.author.did}/post/${getRkey(post)}` + return ( + +
+
+ +
+ +
+ +
+ +

{post.author.displayName}

+ + +

@{post.author.handle}

+ +
+
+ + + +
+ + + + + +
+ {!!post.likeCount && ( +
+ +

+ {post.likeCount} +

+
+ )} + {!!post.repostCount && ( +
+ +

+ {post.repostCount} +

+
+ )} +
+ +

Reply

+
+
+

+ {post.replyCount + ? `Read ${post.replyCount} ${ + post.replyCount > 1 ? 'replies' : 'reply' + } on Bluesky` + : `View on Bluesky`} +

+

+ View on Bluesky +

+
+
+ + ) +} + +function PostContent({record}: {record: AppBskyFeedPost.Record | null}) { + if (!record) return null + + const rt = new RichText({ + text: record.text, + facets: record.facets, + }) + + const richText = [] + + let counter = 0 + for (const segment of rt.segments()) { + if (segment.isLink() && segment.link) { + richText.push( + + {segment.text} + , + ) + } else if (segment.isMention() && segment.mention) { + richText.push( + + {segment.text} + , + ) + } else if (segment.isTag() && segment.tag) { + richText.push( + + {segment.text} + , + ) + } else { + richText.push(segment.text) + } + + counter++ + } + + return ( +

+ {richText} +

+ ) +} diff --git a/bskyembed/src/index.css b/bskyembed/src/index.css new file mode 100644 index 0000000000..23457ec28d --- /dev/null +++ b/bskyembed/src/index.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.break-word { + word-break: break-word; +} \ No newline at end of file diff --git a/bskyembed/src/labels.ts b/bskyembed/src/labels.ts new file mode 100644 index 0000000000..ff3d91bc7b --- /dev/null +++ b/bskyembed/src/labels.ts @@ -0,0 +1,21 @@ +import {AppBskyFeedDefs} from '@atproto/api' + +export const CONTENT_LABELS = ['porn', 'sexual', 'nudity', 'graphic-media'] + +export function labelsToInfo( + labels?: AppBskyFeedDefs.PostView['labels'], +): string | undefined { + const label = labels?.find(label => CONTENT_LABELS.includes(label.val)) + + switch (label?.val) { + case 'porn': + case 'sexual': + return 'Adult Content' + case 'nudity': + return 'Non-sexual Nudity' + case 'graphic-media': + return 'Graphic Media' + default: + return undefined + } +} diff --git a/bskyembed/src/screens/landing.tsx b/bskyembed/src/screens/landing.tsx new file mode 100644 index 0000000000..72612db0ef --- /dev/null +++ b/bskyembed/src/screens/landing.tsx @@ -0,0 +1,294 @@ +import '../index.css' + +import {AppBskyFeedDefs, AppBskyFeedPost, AtUri, BskyAgent} from '@atproto/api' +import {h, render} from 'preact' +import {useEffect, useMemo, useRef, useState} from 'preact/hooks' + +import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg' +import logo from '../../assets/logo.svg' +import {Container} from '../components/container' +import {Link} from '../components/link' +import {Post} from '../components/post' +import {niceDate} from '../utils' + +const DEFAULT_POST = 'https://bsky.app/profile/emilyliu.me/post/3jzn6g7ixgq2y' +const DEFAULT_URI = + 'at://did:plc:vjug55kidv6sye7ykr5faxxn/app.bsky.feed.post/3jzn6g7ixgq2y' + +export const EMBED_SERVICE = 'https://embed.bsky.app' +export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` + +const root = document.getElementById('app') +if (!root) throw new Error('No root element') + +const agent = new BskyAgent({ + service: 'https://public.api.bsky.app', +}) + +render(, root) + +function LandingPage() { + const [uri, setUri] = useState('') + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + const [thread, setThread] = useState( + null, + ) + + useEffect(() => { + void (async () => { + setError(null) + setThread(null) + setLoading(true) + try { + let atUri = DEFAULT_URI + + if (uri) { + if (uri.startsWith('at://')) { + atUri = uri + } else { + try { + const urlp = new URL(uri) + if (!urlp.hostname.endsWith('bsky.app')) { + throw new Error('Invalid hostname') + } + const split = urlp.pathname.slice(1).split('/') + if (split.length < 4) { + throw new Error('Invalid pathname') + } + const [profile, didOrHandle, type, rkey] = split + if (profile !== 'profile' || type !== 'post') { + throw new Error('Invalid profile or type') + } + + let did = didOrHandle + if (!didOrHandle.startsWith('did:')) { + const resolution = await agent.resolveHandle({ + handle: didOrHandle, + }) + if (!resolution.data.did) { + throw new Error('No DID found') + } + did = resolution.data.did + } + + atUri = `at://${did}/app.bsky.feed.post/${rkey}` + } catch (err) { + console.log(err) + throw new Error('Invalid Bluesky URL') + } + } + } + + const {data} = await agent.getPostThread({ + uri: atUri, + depth: 0, + parentHeight: 0, + }) + + if (!AppBskyFeedDefs.isThreadViewPost(data.thread)) { + throw new Error('Post not found') + } + const pwiOptOut = !!data.thread.post.author.labels?.find( + label => label.val === '!no-unauthenticated', + ) + if (pwiOptOut) { + throw new Error( + 'The author of this post has requested their posts not be displayed on external sites.', + ) + } + setThread(data.thread) + } catch (err) { + console.error(err) + setError(err instanceof Error ? err.message : 'Invalid Bluesky URL') + } finally { + setLoading(false) + } + })() + }, [uri]) + + return ( +
+ + + + +

Embed a Bluesky Post

+ + setUri(e.currentTarget.value)} + className="border rounded-lg py-3 w-full max-w-[600px] px-4" + placeholder={DEFAULT_POST} + /> + + + + {loading ? ( + + ) : ( +
+ {!error && thread && uri && } + {!error && thread && } + {error && ( +
+

{error}

+
+ )} +
+ )} +
+ ) +} + +function Skeleton() { + return ( + +
+
+
+
+
+
+
+
+
+
+
+
+ + ) +} + +function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) { + const ref = useRef(null) + const [copied, setCopied] = useState(false) + + // reset copied state after 2 seconds + useEffect(() => { + if (copied) { + const timeout = setTimeout(() => { + setCopied(false) + }, 2000) + return () => clearTimeout(timeout) + } + }, [copied]) + + const snippet = useMemo(() => { + const record = thread.post.record + + if (!AppBskyFeedPost.isRecord(record)) { + return '' + } + + const lang = record.langs && record.langs.length > 0 ? record.langs[0] : '' + const profileHref = toShareUrl( + ['/profile', thread.post.author.did].join('/'), + ) + const urip = new AtUri(thread.post.uri) + const href = toShareUrl( + ['/profile', thread.post.author.did, 'post', urip.rkey].join('/'), + ) + + // x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x + // DO NOT ADD ANY NEW INTERPOLATIONS BELOW WITHOUT ESCAPING THEM! + // Also, keep this code synced with the app code in Embed.tsx. + // x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x + return `

${escapeHtml(record.text)}${ + record.embed + ? `

[image or embed]` + : '' + }

— ${escapeHtml( + thread.post.author.displayName || thread.post.author.handle, + )} (
@${escapeHtml( + thread.post.author.handle, + )}) ${escapeHtml( + niceDate(thread.post.indexedAt), + )}
` + }, [thread]) + + return ( +
+ { + ref.current?.select() + }} + /> + +
+ ) +} + +function toShareUrl(path: string) { + return `https://bsky.app${path}?ref_src=embed` +} + +/** + * Based on a snippet of code from React, which itself was based on the escape-html library. + * Copyright (c) Meta Platforms, Inc. and affiliates + * Copyright (c) 2012-2013 TJ Holowaychuk + * Copyright (c) 2015 Andreas Lubbe + * Copyright (c) 2015 Tiancheng "Timothy" Gu + * Licensed as MIT. + */ +const matchHtmlRegExp = /["'&<>]/ +function escapeHtml(string: string) { + const str = String(string) + const match = matchHtmlRegExp.exec(str) + if (!match) { + return str + } + let escape + let html = '' + let index + let lastIndex = 0 + for (index = match.index; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: // " + escape = '"' + break + case 38: // & + escape = '&' + break + case 39: // ' + escape = ''' + break + case 60: // < + escape = '<' + break + case 62: // > + escape = '>' + break + default: + continue + } + if (lastIndex !== index) { + html += str.slice(lastIndex, index) + } + lastIndex = index + 1 + html += escape + } + return lastIndex !== index ? html + str.slice(lastIndex, index) : html +} diff --git a/bskyembed/src/screens/post.tsx b/bskyembed/src/screens/post.tsx new file mode 100644 index 0000000000..365227cd47 --- /dev/null +++ b/bskyembed/src/screens/post.tsx @@ -0,0 +1,85 @@ +import '../index.css' + +import {AppBskyFeedDefs, BskyAgent} from '@atproto/api' +import {h, render} from 'preact' + +import logo from '../../assets/logo.svg' +import {Container} from '../components/container' +import {Link} from '../components/link' +import {Post} from '../components/post' +import {getRkey} from '../utils' + +const root = document.getElementById('app') +if (!root) throw new Error('No root element') + +const agent = new BskyAgent({ + service: 'https://public.api.bsky.app', +}) + +const uri = `at://${window.location.pathname.slice('/embed/'.length)}` +if (!uri) { + throw new Error('No uri in path') +} + +agent + .getPostThread({ + uri, + depth: 0, + parentHeight: 0, + }) + .then(({data}) => { + if (!AppBskyFeedDefs.isThreadViewPost(data.thread)) { + throw new Error('Expected a ThreadViewPost') + } + const pwiOptOut = !!data.thread.post.author.labels?.find( + label => label.val === '!no-unauthenticated', + ) + if (pwiOptOut) { + render(, root) + } else { + render(, root) + } + }) + .catch(err => { + console.error(err) + render(, root) + }) + +function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) { + const href = `/profile/${thread.post.author.did}/post/${getRkey(thread.post)}` + return ( + + + + +
+

+ The author of this post has requested their posts not be displayed on + external sites. +

+ + View on Bluesky + +
+
+ ) +} + +function ErrorMessage() { + return ( + + + + +

+ Post not found, it may have been deleted. +

+
+ ) +} diff --git a/bskyembed/src/utils.ts b/bskyembed/src/utils.ts new file mode 100644 index 0000000000..1f6fd5061c --- /dev/null +++ b/bskyembed/src/utils.ts @@ -0,0 +1,18 @@ +import {AtUri} from '@atproto/api' + +export function niceDate(date: number | string | Date) { + const d = new Date(date) + return `${d.toLocaleDateString('en-us', { + year: 'numeric', + month: 'short', + day: 'numeric', + })} at ${d.toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + })}` +} + +export function getRkey({uri}: {uri: string}): string { + const at = new AtUri(uri) + return at.rkey +} diff --git a/bskyembed/tailwind.config.cjs b/bskyembed/tailwind.config.cjs new file mode 100644 index 0000000000..092e8c2cb7 --- /dev/null +++ b/bskyembed/tailwind.config.cjs @@ -0,0 +1,13 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + brand: 'rgb(10,122,255)', + textLight: 'rgb(66,87,108)', + }, + }, + }, + plugins: [], +} diff --git a/bskyembed/tsconfig.json b/bskyembed/tsconfig.json new file mode 100644 index 0000000000..44c516ed11 --- /dev/null +++ b/bskyembed/tsconfig.json @@ -0,0 +1,24 @@ + +{ + "compilerOptions": { + "target": "ES5", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "types": ["vite/client"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment", + "downlevelIteration": true + }, + "include": ["src"] +} diff --git a/bskyembed/tsconfig.snippet.json b/bskyembed/tsconfig.snippet.json new file mode 100644 index 0000000000..a6b6071dd6 --- /dev/null +++ b/bskyembed/tsconfig.snippet.json @@ -0,0 +1,10 @@ + +{ + "compilerOptions": { + "target": "ES5", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "strict": true, + "outDir": "dist" + }, + "include": ["snippet"], +} diff --git a/bskyembed/vite.config.ts b/bskyembed/vite.config.ts new file mode 100644 index 0000000000..9acc9d5ee4 --- /dev/null +++ b/bskyembed/vite.config.ts @@ -0,0 +1,27 @@ +import {resolve} from 'node:path' + +import preact from '@preact/preset-vite' +import legacy from '@vitejs/plugin-legacy' +import type {UserConfig} from 'vite' +import paths from 'vite-tsconfig-paths' + +const config: UserConfig = { + plugins: [ + preact(), + paths(), + legacy({ + targets: ['defaults', 'not IE 11'], + }), + ], + build: { + assetsDir: 'static', + rollupOptions: { + input: { + index: resolve(__dirname, 'index.html'), + post: resolve(__dirname, 'post.html'), + }, + }, + }, +} + +export default config diff --git a/bskyembed/yarn.lock b/bskyembed/yarn.lock new file mode 100644 index 0000000000..60efe36845 --- /dev/null +++ b/bskyembed/yarn.lock @@ -0,0 +1,4194 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@atproto/api@^0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.2.tgz#5df6d4f60dea0395c84fdebd9e81a7e853edf130" + integrity sha512-UVzCiDZH2j0wrr/O8nb1edD5cYLVqB5iujueXUCbHS3rAwIxgmyLtA3Hzm2QYsGPo/+xsIg1fNvpq9rNT6KWUA== + 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/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" + +"@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" + integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== + dependencies: + "@babel/highlight" "^7.24.2" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" + integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== + +"@babel/core@^7.13.16", "@babel/core@^7.22.1", "@babel/core@^7.23.9": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.4.tgz#1f758428e88e0d8c563874741bc4ffc4f71a4717" + integrity sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.2" + "@babel/generator" "^7.24.4" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helpers" "^7.24.4" + "@babel/parser" "^7.24.4" + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.1" + "@babel/types" "^7.24.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/eslint-parser@^7.13.14": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.24.1.tgz#e27eee93ed1d271637165ef3a86e2b9332395c32" + integrity sha512-d5guuzMlPeDfZIbpQ8+g1NaCNuAGBBGNECh0HVqz1sjOeVLh2CEaifuOysCH18URW6R7pqXINvf5PaR/dC6jLQ== + dependencies: + "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" + eslint-visitor-keys "^2.1.0" + semver "^6.3.1" + +"@babel/generator@^7.24.1", "@babel/generator@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.4.tgz#1fc55532b88adf952025d5d2d1e71f946cb1c498" + integrity sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw== + dependencies: + "@babel/types" "^7.24.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" + integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== + dependencies: + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.24.1", "@babel/helper-create-class-features-plugin@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz#c806f73788a6800a5cfbbc04d2df7ee4d927cce3" + integrity sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-member-expression-to-functions" "^7.23.0" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.24.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.15", "@babel/helper-create-regexp-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" + integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz#fadc63f0c2ff3c8d02ed905dcea747c5b0fb74fd" + integrity sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-member-expression-to-functions@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" + integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== + dependencies: + "@babel/types" "^7.23.0" + +"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.24.1": + version "7.24.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128" + integrity sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg== + dependencies: + "@babel/types" "^7.24.0" + +"@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz#945681931a52f15ce879fd5b86ce2dae6d3d7f2a" + integrity sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w== + +"@babel/helper-remap-async-to-generator@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" + integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-wrap-function" "^7.22.20" + +"@babel/helper-replace-supers@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz#7085bd19d4a0b7ed8f405c1ed73ccb70f323abc1" + integrity sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-member-expression-to-functions" "^7.23.0" + "@babel/helper-optimise-call-expression" "^7.22.5" + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.23.4": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" + integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== + +"@babel/helper-wrap-function@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569" + integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw== + dependencies: + "@babel/helper-function-name" "^7.22.5" + "@babel/template" "^7.22.15" + "@babel/types" "^7.22.19" + +"@babel/helpers@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.4.tgz#dc00907fd0d95da74563c142ef4cd21f2cb856b6" + integrity sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw== + dependencies: + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.1" + "@babel/types" "^7.24.0" + +"@babel/highlight@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" + integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/parser@^7.24.0", "@babel/parser@^7.24.1", "@babel/parser@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88" + integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz#6125f0158543fb4edf1c22f322f3db67f21cb3e1" + integrity sha512-qpl6vOOEEzTLLcsuqYYo8yDtrTocmu2xkGvgNebvPjT9DTtfFYGmgDqY+rBYXNlqL4s9qLDn6xkrJv4RxAPiTA== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz#b645d9ba8c2bc5b7af50f0fe949f9edbeb07c8cf" + integrity sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz#da8261f2697f0f41b0855b91d3a20a1fbfd271d3" + integrity sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.24.1" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz#1181d9685984c91d657b8ddf14f0487a6bab2988" + integrity sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-decorators@^7.12.13": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz#71d9ad06063a6ac5430db126b5df48c70ee885fa" + integrity sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz#db3aad724153a00eaac115a3fb898de544e34971" + integrity sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-syntax-import-attributes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz#c66b966c63b714c4eec508fcf5763b1f2d381093" + integrity sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.12.13", "@babel/plugin-syntax-jsx@^7.23.3": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz#3f6ca04b8c841811dbc3c5c5f837934e0d626c10" + integrity sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz#2bf263617060c9cc45bcdbf492b8cc805082bf27" + integrity sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-async-generator-functions@^7.24.3": + version "7.24.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz#8fa7ae481b100768cc9842c8617808c5352b8b89" + integrity sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-remap-async-to-generator" "^7.22.20" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-transform-async-to-generator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz#0e220703b89f2216800ce7b1c53cb0cf521c37f4" + integrity sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw== + dependencies: + "@babel/helper-module-imports" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-remap-async-to-generator" "^7.22.20" + +"@babel/plugin-transform-block-scoped-functions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz#1c94799e20fcd5c4d4589523bbc57b7692979380" + integrity sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-block-scoping@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz#28f5c010b66fbb8ccdeef853bef1935c434d7012" + integrity sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-class-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz#bcbf1aef6ba6085cfddec9fc8d58871cf011fc29" + integrity sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-class-static-block@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz#1a4653c0cf8ac46441ec406dece6e9bc590356a4" + integrity sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.4" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-transform-classes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz#5bc8fc160ed96378184bc10042af47f50884dcb1" + integrity sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-replace-supers" "^7.24.1" + "@babel/helper-split-export-declaration" "^7.22.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz#bc7e787f8e021eccfb677af5f13c29a9934ed8a7" + integrity sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/template" "^7.24.0" + +"@babel/plugin-transform-destructuring@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz#b1e8243af4a0206841973786292b8c8dd8447345" + integrity sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-dotall-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz#d56913d2f12795cc9930801b84c6f8c47513ac13" + integrity sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-duplicate-keys@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz#5347a797fe82b8d09749d10e9f5b83665adbca88" + integrity sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-dynamic-import@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz#2a5a49959201970dd09a5fca856cb651e44439dd" + integrity sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-exponentiation-operator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz#6650ebeb5bd5c012d5f5f90a26613a08162e8ba4" + integrity sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-export-namespace-from@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz#f033541fc036e3efb2dcb58eedafd4f6b8078acd" + integrity sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-transform-for-of@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz#67448446b67ab6c091360ce3717e7d3a59e202fd" + integrity sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + +"@babel/plugin-transform-function-name@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz#8cba6f7730626cc4dfe4ca2fa516215a0592b361" + integrity sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA== + dependencies: + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-json-strings@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz#08e6369b62ab3e8a7b61089151b161180c8299f7" + integrity sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-transform-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz#0a1982297af83e6b3c94972686067df588c5c096" + integrity sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-logical-assignment-operators@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz#719d8aded1aa94b8fb34e3a785ae8518e24cfa40" + integrity sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz#896d23601c92f437af8b01371ad34beb75df4489" + integrity sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-modules-amd@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz#b6d829ed15258536977e9c7cc6437814871ffa39" + integrity sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ== + dependencies: + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-modules-commonjs@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz#e71ba1d0d69e049a22bf90b3867e263823d3f1b9" + integrity sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw== + dependencies: + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-simple-access" "^7.22.5" + +"@babel/plugin-transform-modules-systemjs@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz#2b9625a3d4e445babac9788daec39094e6b11e3e" + integrity sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA== + dependencies: + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/plugin-transform-modules-umd@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz#69220c66653a19cf2c0872b9c762b9a48b8bebef" + integrity sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg== + dependencies: + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" + integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-new-target@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz#29c59988fa3d0157de1c871a28cd83096363cc34" + integrity sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz#0cd494bb97cb07d428bd651632cb9d4140513988" + integrity sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz#5bc019ce5b3435c1cadf37215e55e433d674d4e8" + integrity sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz#5a3ce73caf0e7871a02e1c31e8b473093af241ff" + integrity sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA== + dependencies: + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.24.1" + +"@babel/plugin-transform-object-super@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz#e71d6ab13483cca89ed95a474f542bbfc20a0520" + integrity sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-replace-supers" "^7.24.1" + +"@babel/plugin-transform-optional-catch-binding@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz#92a3d0efe847ba722f1a4508669b23134669e2da" + integrity sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz#26e588acbedce1ab3519ac40cc748e380c5291e6" + integrity sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-transform-parameters@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz#983c15d114da190506c75b616ceb0f817afcc510" + integrity sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-private-methods@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz#a0faa1ae87eff077e1e47a5ec81c3aef383dc15a" + integrity sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-private-property-in-object@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz#756443d400274f8fb7896742962cc1b9f25c1f6a" + integrity sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz#d6a9aeab96f03749f4eebeb0b6ea8e90ec958825" + integrity sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-react-jsx-development@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" + integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.22.5" + +"@babel/plugin-transform-react-jsx@^7.22.15", "@babel/plugin-transform-react-jsx@^7.22.5": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz#393f99185110cea87184ea47bcb4a7b0c2e39312" + integrity sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.23.3" + "@babel/types" "^7.23.4" + +"@babel/plugin-transform-regenerator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz#625b7545bae52363bdc1fbbdc7252b5046409c8c" + integrity sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-reserved-words@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz#8de729f5ecbaaf5cf83b67de13bad38a21be57c1" + integrity sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-shorthand-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz#ba9a09144cf55d35ec6b93a32253becad8ee5b55" + integrity sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-spread@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz#a1acf9152cbf690e4da0ba10790b3ac7d2b2b391" + integrity sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + +"@babel/plugin-transform-sticky-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz#f03e672912c6e203ed8d6e0271d9c2113dc031b9" + integrity sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-template-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz#15e2166873a30d8617e3e2ccadb86643d327aab7" + integrity sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-typeof-symbol@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz#6831f78647080dec044f7e9f68003d99424f94c7" + integrity sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-unicode-escapes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz#fb3fa16676549ac7c7449db9b342614985c2a3a4" + integrity sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-unicode-property-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz#56704fd4d99da81e5e9f0c0c93cabd91dbc4889e" + integrity sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-unicode-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz#57c3c191d68f998ac46b708380c1ce4d13536385" + integrity sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-transform-unicode-sets-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz#c1ea175b02afcffc9cf57a9c4658326625165b7f" + integrity sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/preset-env@^7.23.9": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.4.tgz#46dbbcd608771373b88f956ffb67d471dce0d23b" + integrity sha512-7Kl6cSmYkak0FK/FXjSEnLJ1N9T/WA2RkMhu17gZ/dsxKJUuTYNIylahPTzqpLyJN4WhDif8X0XK1R8Wsguo/A== + dependencies: + "@babel/compat-data" "^7.24.4" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-option" "^7.23.5" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.4" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.24.1" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.24.1" + "@babel/plugin-syntax-import-attributes" "^7.24.1" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.24.1" + "@babel/plugin-transform-async-generator-functions" "^7.24.3" + "@babel/plugin-transform-async-to-generator" "^7.24.1" + "@babel/plugin-transform-block-scoped-functions" "^7.24.1" + "@babel/plugin-transform-block-scoping" "^7.24.4" + "@babel/plugin-transform-class-properties" "^7.24.1" + "@babel/plugin-transform-class-static-block" "^7.24.4" + "@babel/plugin-transform-classes" "^7.24.1" + "@babel/plugin-transform-computed-properties" "^7.24.1" + "@babel/plugin-transform-destructuring" "^7.24.1" + "@babel/plugin-transform-dotall-regex" "^7.24.1" + "@babel/plugin-transform-duplicate-keys" "^7.24.1" + "@babel/plugin-transform-dynamic-import" "^7.24.1" + "@babel/plugin-transform-exponentiation-operator" "^7.24.1" + "@babel/plugin-transform-export-namespace-from" "^7.24.1" + "@babel/plugin-transform-for-of" "^7.24.1" + "@babel/plugin-transform-function-name" "^7.24.1" + "@babel/plugin-transform-json-strings" "^7.24.1" + "@babel/plugin-transform-literals" "^7.24.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.1" + "@babel/plugin-transform-member-expression-literals" "^7.24.1" + "@babel/plugin-transform-modules-amd" "^7.24.1" + "@babel/plugin-transform-modules-commonjs" "^7.24.1" + "@babel/plugin-transform-modules-systemjs" "^7.24.1" + "@babel/plugin-transform-modules-umd" "^7.24.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.24.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.1" + "@babel/plugin-transform-numeric-separator" "^7.24.1" + "@babel/plugin-transform-object-rest-spread" "^7.24.1" + "@babel/plugin-transform-object-super" "^7.24.1" + "@babel/plugin-transform-optional-catch-binding" "^7.24.1" + "@babel/plugin-transform-optional-chaining" "^7.24.1" + "@babel/plugin-transform-parameters" "^7.24.1" + "@babel/plugin-transform-private-methods" "^7.24.1" + "@babel/plugin-transform-private-property-in-object" "^7.24.1" + "@babel/plugin-transform-property-literals" "^7.24.1" + "@babel/plugin-transform-regenerator" "^7.24.1" + "@babel/plugin-transform-reserved-words" "^7.24.1" + "@babel/plugin-transform-shorthand-properties" "^7.24.1" + "@babel/plugin-transform-spread" "^7.24.1" + "@babel/plugin-transform-sticky-regex" "^7.24.1" + "@babel/plugin-transform-template-literals" "^7.24.1" + "@babel/plugin-transform-typeof-symbol" "^7.24.1" + "@babel/plugin-transform-unicode-escapes" "^7.24.1" + "@babel/plugin-transform-unicode-property-regex" "^7.24.1" + "@babel/plugin-transform-unicode-regex" "^7.24.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.24.1" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.4" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.31.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime@^7.8.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.22.15", "@babel/template@^7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" + integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.24.0" + "@babel/types" "^7.24.0" + +"@babel/traverse@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.1.tgz#d65c36ac9dd17282175d1e4a3c49d5b7988f530c" + integrity sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ== + dependencies: + "@babel/code-frame" "^7.24.1" + "@babel/generator" "^7.24.1" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.24.1" + "@babel/types" "^7.24.0" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.4", "@babel/types@^7.24.0", "@babel/types@^7.4.4": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" + integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== + +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@mdn/browser-compat-data@^5.2.34", "@mdn/browser-compat-data@^5.3.13": + version "5.5.19" + resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.5.19.tgz#5c661edd669ee990dbdf2e1a8ee3c9c1c6fa7117" + integrity sha512-ntKBZtwWCy4XvJosdTJKqIMdmzgbxjopfoiMxgpzsml3dXqA7MIHCE/amidfQc06a6KvmMrpiVuYHIBt2feDog== + +"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": + version "5.1.1-v1" + resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" + integrity sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg== + dependencies: + eslint-scope "5.1.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@preact/preset-vite@^2.8.2": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@preact/preset-vite/-/preset-vite-2.8.2.tgz#8113a37c8f7dfa0156fe1e6811bdc2b7ea6a3490" + integrity sha512-m3tl+M8IO8jgiHnk+7LSTFl8axdPXloewi7iGVLdmCwf34XOzEUur0bZVewW4DUbUipFjTS2CXu27+5f/oexBA== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.22.15" + "@babel/plugin-transform-react-jsx-development" "^7.22.5" + "@prefresh/vite" "^2.4.1" + "@rollup/pluginutils" "^4.1.1" + babel-plugin-transform-hook-names "^1.0.2" + debug "^4.3.4" + kolorist "^1.8.0" + magic-string "0.30.5" + node-html-parser "^6.1.10" + resolve "^1.22.8" + source-map "^0.7.4" + stack-trace "^1.0.0-pre2" + +"@prefresh/babel-plugin@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@prefresh/babel-plugin/-/babel-plugin-0.5.1.tgz#3161bbbf12dd39a5fe08514349898fa6a20525b7" + integrity sha512-uG3jGEAysxWoyG3XkYfjYHgaySFrSsaEb4GagLzYaxlydbuREtaX+FTxuIidp241RaLl85XoHg9Ej6E4+V1pcg== + +"@prefresh/core@^1.5.1": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@prefresh/core/-/core-1.5.2.tgz#750e1936d82f3b0a1199d3cda5c35e3443128490" + integrity sha512-A/08vkaM1FogrCII5PZKCrygxSsc11obExBScm3JF1CryK2uDS3ZXeni7FeKCx1nYdUkj4UcJxzPzc1WliMzZA== + +"@prefresh/utils@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@prefresh/utils/-/utils-1.2.0.tgz#cbdfe549b207041e38bb6cc382408b30cd24fec8" + integrity sha512-KtC/fZw+oqtwOLUFM9UtiitB0JsVX0zLKNyRTA332sqREqSALIIQQxdUCS1P3xR/jT1e2e8/5rwH6gdcMLEmsQ== + +"@prefresh/vite@^2.4.1": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@prefresh/vite/-/vite-2.4.5.tgz#8e6ecdb36510b8497c346a5a7f55e0bc9b9b5f6b" + integrity sha512-iForDVJ2M8gQYnm5pHumvTEJjGGc7YNYC0GVKnHFL+GvFfKHfH9Rpq67nUAzNbjuLEpqEOUuQVQajMazWu2ZNQ== + dependencies: + "@babel/core" "^7.22.1" + "@prefresh/babel-plugin" "0.5.1" + "@prefresh/core" "^1.5.1" + "@prefresh/utils" "^1.2.0" + "@rollup/pluginutils" "^4.2.1" + +"@rollup/pluginutils@^4.1.1", "@rollup/pluginutils@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +"@rollup/rollup-android-arm-eabi@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.1.tgz#ca0501dd836894216cb9572848c5dde4bfca3bec" + integrity sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA== + +"@rollup/rollup-android-arm64@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.1.tgz#154ca7e4f815d2e442ffc62ee7f64aee8b2547b0" + integrity sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ== + +"@rollup/rollup-darwin-arm64@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.1.tgz#02b522ab6ccc2c504634651985ff8e657b42c055" + integrity sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q== + +"@rollup/rollup-darwin-x64@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.1.tgz#217737f9f73de729fdfd7d529afebb6c8283f554" + integrity sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA== + +"@rollup/rollup-linux-arm-gnueabihf@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.1.tgz#a87e478ab3f697c7f4e74c8b1cac1e0667f8f4be" + integrity sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g== + +"@rollup/rollup-linux-arm64-gnu@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.1.tgz#4da6830eca27e5f4ca15f9197e5660952ca185c6" + integrity sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w== + +"@rollup/rollup-linux-arm64-musl@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.1.tgz#0b0ed35720aebc8f5e501d370a9ea0f686ead1e0" + integrity sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw== + +"@rollup/rollup-linux-powerpc64le-gnu@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.1.tgz#024ad04d162726f25e62915851f7df69a9677c17" + integrity sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw== + +"@rollup/rollup-linux-riscv64-gnu@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.1.tgz#180694d1cd069ddbe22022bb5b1bead3b7de581c" + integrity sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw== + +"@rollup/rollup-linux-s390x-gnu@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.1.tgz#f7b4e2b0ca49be4e34f9ef0b548c926d94edee87" + integrity sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA== + +"@rollup/rollup-linux-x64-gnu@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.1.tgz#0aaf79e5b9ccf7db3084fe6c3f2d2873a27d5af4" + integrity sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA== + +"@rollup/rollup-linux-x64-musl@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.1.tgz#38f0a37ca5015eb07dff86a1b6f94279c179f4ed" + integrity sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g== + +"@rollup/rollup-win32-arm64-msvc@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.1.tgz#84d48c55740ede42c77373f76e85f368633a0cc3" + integrity sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA== + +"@rollup/rollup-win32-ia32-msvc@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.1.tgz#c1e0bc39e20e760f0a526ddf14ae0543af796605" + integrity sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg== + +"@rollup/rollup-win32-x64-msvc@4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.1.tgz#299eee74b7d87e116083ac5b1ce8dd9434668294" + integrity sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew== + +"@types/estree@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/semver@^7.3.12": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + +"@typescript-eslint/experimental-utils@^5.0.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz#14559bf73383a308026b427a4a6129bae2146741" + integrity sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw== + dependencies: + "@typescript-eslint/utils" "5.62.0" + +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@vitejs/plugin-legacy@^5.3.2": + version "5.3.2" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-legacy/-/plugin-legacy-5.3.2.tgz#f890db6014898c36af85b8ad52c680ef026b8aa8" + integrity sha512-8moCOrIMaZ/Rjln0Q6GsH6s8fAt1JOI3k8nmfX4tXUxE5KAExVctSyOBk+A25GClsdSWqIk2yaUthH3KJ2X4tg== + dependencies: + "@babel/core" "^7.23.9" + "@babel/preset-env" "^7.23.9" + browserslist "^4.23.0" + browserslist-to-esbuild "^2.1.1" + core-js "^3.36.0" + magic-string "^0.30.7" + regenerator-runtime "^0.14.1" + systemjs "^6.14.3" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.8.2, acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-includes@^3.1.6, array-includes@^3.1.7: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.findlast@^1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.toreversed@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" + integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" + integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.1.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +ast-metadata-inferer@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/ast-metadata-inferer/-/ast-metadata-inferer-0.8.0.tgz#0f94c3425e310d8da45823ab2161142e3f134343" + integrity sha512-jOMKcHht9LxYIEQu+RVd22vtgrPaVCtDRQ/16IGmurdzxvYbDd5ynxjnyrzLnieG96eTcAyaoj/wN/4/1FyyeA== + dependencies: + "@mdn/browser-compat-data" "^5.2.34" + +autoprefixer@^10.4.19: + version "10.4.19" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" + integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== + dependencies: + browserslist "^4.23.0" + caniuse-lite "^1.0.30001599" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.10" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz#276f41710b03a64f6467433cab72cbc2653c38b1" + integrity sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.6.1" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.10.4: + version "0.10.4" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" + integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.1" + core-js-compat "^3.36.1" + +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz#4f08ef4c62c7a7f66a35ed4c0d75e30506acc6be" + integrity sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.1" + +babel-plugin-transform-hook-names@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz#0d75c2d78e8bbcdb258241131562b9cf07f010f3" + integrity sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist-to-esbuild@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/browserslist-to-esbuild/-/browserslist-to-esbuild-2.1.1.tgz#50dc4c55a6889ba22c7b1bd820032f81b822faf0" + integrity sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw== + dependencies: + meow "^13.0.0" + +browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.23.0: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== + dependencies: + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, 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" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +caniuse-lite@^1.0.30001524, caniuse-lite@^1.0.30001587: + version "1.0.30001606" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz#b4d5f67ab0746a3b8b5b6d1f06e39c51beb39a9e" + integrity sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg== + +caniuse-lite@^1.0.30001599: + version "1.0.30001607" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001607.tgz#b91e8e033f6bca4e13d3d45388d87fa88931d9a5" + integrity sha512-WcvhVRjXLKFB/kmOFVwELtMxyhq3iM/MvmXcyCe2PNf166c39mptscOc/45TTS96n2gpNV2z7+NakArTWZCQ3w== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +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== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js-compat@^3.31.0, core-js-compat@^3.36.1: + version "3.36.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.36.1.tgz#1818695d72c99c25d621dca94e6883e190cea3c8" + integrity sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA== + dependencies: + browserslist "^4.23.0" + +core-js@^3.36.0: + version "3.36.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.36.1.tgz#c97a7160ebd00b2de19e62f4bbd3406ab720e578" + integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA== + +cross-spawn@^7.0.0, cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +define-data-property@^1.0.1, 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" + +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +electron-to-chromium@^1.4.668: + version "1.4.728" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.728.tgz#ac54d9d1b38752b920ec737a48c83dec2bf45ea1" + integrity sha512-Ud1v7hJJYIqehlUJGqR6PF1Ek8l80zWwxA6nGxigBsGJ9f9M2fciHyrIiNMerSHSH3p+0/Ia7jIlnDkt41h5cw== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +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.1.0, es-errors@^1.2.1, 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== + +es-iterator-helpers@^1.0.17: + version "1.0.18" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz#4d3424f46b24df38d064af6fbbc89274e29ea69d" + integrity sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +esbuild@^0.20.1: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== + optionalDependencies: + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-preact@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-preact/-/eslint-config-preact-1.3.0.tgz#17b72813078f4d1d4d2b79938ec21f92338bc9c0" + integrity sha512-yHYXg5qNzEJd3D/30AmsIW0W8MuY858KpApXp7xxBF08IYUljSKCOqMx+dVucXHQnAm7+11wOnMkgVHIBAechw== + dependencies: + "@babel/core" "^7.13.16" + "@babel/eslint-parser" "^7.13.14" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-decorators" "^7.12.13" + "@babel/plugin-syntax-jsx" "^7.12.13" + eslint-plugin-compat "^4.0.0" + eslint-plugin-jest "^25.2.4" + eslint-plugin-react "^7.27.0" + eslint-plugin-react-hooks "^4.3.0" + +eslint-plugin-compat@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-compat/-/eslint-plugin-compat-4.2.0.tgz#eeaf80daa1afe495c88a47e9281295acae45c0aa" + integrity sha512-RDKSYD0maWy5r7zb5cWQS+uSPc26mgOzdORJ8hxILmWM7S/Ncwky7BcAtXVY5iRbKjBdHsWU8Yg7hfoZjtkv7w== + dependencies: + "@mdn/browser-compat-data" "^5.3.13" + ast-metadata-inferer "^0.8.0" + browserslist "^4.21.10" + caniuse-lite "^1.0.30001524" + find-up "^5.0.0" + lodash.memoize "^4.1.2" + semver "^7.5.4" + +eslint-plugin-jest@^25.2.4: + version "25.7.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz#ff4ac97520b53a96187bad9c9814e7d00de09a6a" + integrity sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ== + dependencies: + "@typescript-eslint/experimental-utils" "^5.0.0" + +eslint-plugin-react-hooks@^4.3.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + +eslint-plugin-react@^7.27.0: + version "7.34.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" + integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlast "^1.2.4" + array.prototype.flatmap "^1.3.2" + array.prototype.toreversed "^1.1.2" + array.prototype.tosorted "^1.1.3" + doctrine "^2.1.0" + es-iterator-helpers "^1.0.17" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.7" + object.fromentries "^2.0.7" + object.hasown "^1.1.3" + object.values "^1.1.7" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.10" + +eslint-plugin-simple-import-sort@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.0.0.tgz#3cfa05d74509bd4dc329a956938823812194dbb6" + integrity sha512-8o0dVEdAkYap0Cn5kNeklaKcT1nUsa3LITWEuFk3nJifOoD+5JQGoyDUW2W/iPWwBsNBJpyJS9y4je/BgxLcyQ== + +eslint-scope@5.1.1, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.19.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9, fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +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== + +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.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" + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^10.3.10: + version "10.3.12" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" + integrity sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.6" + minimatch "^9.0.1" + minipass "^7.0.4" + path-scurry "^1.10.2" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +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-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, 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, has-proto@^1.0.3: + 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.2, 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== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + 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" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +ignore@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1, is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +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== + +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +jackspeak@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jiti@^1.21.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^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== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kolorist@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c" + integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lilconfig@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +lilconfig@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.1.tgz#9d8a246fa753106cfc205fd2d77042faca56e5e3" + integrity sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +loose-envify@^1.4.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" + +lru-cache@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +magic-string@0.30.5: + version "0.30.5" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" + integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +magic-string@^0.30.7: + version "0.30.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.9.tgz#8927ae21bfdd856310e07a1bc8dd5e73cb6c251d" + integrity sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +meow@^13.0.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" + integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.1: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multiformats@^9.4.2, 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== + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-html-parser@^6.1.10: + version "6.1.13" + resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.13.tgz#a1df799b83df5c6743fcd92740ba14682083b7e4" + integrity sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg== + dependencies: + css-select "^5.1.0" + he "1.2.0" + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +object-assign@^4.0.1, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +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== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +object.fromentries@^2.0.7: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.hasown@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" + integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== + dependencies: + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.values@^1.1.6, object.values@^1.1.7: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" + integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pirates@^4.0.1: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" + integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== + dependencies: + camelcase-css "^2.0.1" + +postcss-load-config@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== + dependencies: + lilconfig "^3.0.0" + yaml "^2.3.4" + +postcss-nested@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" + integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== + dependencies: + postcss-selector-parser "^6.0.11" + +postcss-selector-parser@^6.0.11: + version "6.0.16" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04" + integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.0.0, 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== + +postcss@^8.4.23, postcss@^8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + +preact@^10.4.8: + version "10.20.1" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.20.1.tgz#1bc598ab630d8612978f7533da45809a8298542b" + integrity sha512-JIFjgFg9B2qnOoGiYMVBtrcFxHqn+dNXbq76bVmcaHYJFYR4lW67AOcXgAYQQTDYXDOg/kTZrKPNCdRgJ2UJmw== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + +regenerate-unicode-properties@^10.1.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.14.0, regenerator-runtime@^0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.1.7, resolve@^1.14.2, resolve@^1.22.2, resolve@^1.22.8: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup@^4.13.0: + version "4.14.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.14.1.tgz#228d5159c3f4d8745bd24819d734bc6c6ca87c09" + integrity sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.14.1" + "@rollup/rollup-android-arm64" "4.14.1" + "@rollup/rollup-darwin-arm64" "4.14.1" + "@rollup/rollup-darwin-x64" "4.14.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.14.1" + "@rollup/rollup-linux-arm64-gnu" "4.14.1" + "@rollup/rollup-linux-arm64-musl" "4.14.1" + "@rollup/rollup-linux-powerpc64le-gnu" "4.14.1" + "@rollup/rollup-linux-riscv64-gnu" "4.14.1" + "@rollup/rollup-linux-s390x-gnu" "4.14.1" + "@rollup/rollup-linux-x64-gnu" "4.14.1" + "@rollup/rollup-linux-x64-musl" "4.14.1" + "@rollup/rollup-win32-arm64-msvc" "4.14.1" + "@rollup/rollup-win32-ia32-msvc" "4.14.1" + "@rollup/rollup-win32-x64-msvc" "4.14.1" + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.7, semver@^7.5.4: + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + dependencies: + lru-cache "^6.0.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" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4, side-channel@^1.0.6: + 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" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +stack-trace@^1.0.0-pre2: + version "1.0.0-pre2" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-1.0.0-pre2.tgz#46a83a79f1b287807e9aaafc6a5dd8bcde626f9c" + integrity sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A== + +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.matchall@^4.0.10: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +sucrase@^3.32.0: + version "3.35.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + glob "^10.3.10" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +systemjs@^6.14.3: + version "6.14.3" + resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.14.3.tgz#c1d6e4ff5f9ff7106e5bb3d451360b1a066bde8a" + integrity sha512-hQv45irdhXudAOr8r6SVSpJSGtogdGZUbJBRKCE5nsIS7tsxxvnIHqT4IOPWj+P+HcSzeWzHlGCGpmhPDIKe+w== + +tailwindcss@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.3.tgz#be48f5283df77dfced705451319a5dffb8621519" + integrity sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A== + dependencies: + "@alloc/quick-lru" "^5.2.0" + arg "^5.0.2" + chokidar "^3.5.3" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.3.0" + glob-parent "^6.0.2" + is-glob "^4.0.3" + jiti "^1.21.0" + lilconfig "^2.1.0" + micromatch "^4.0.5" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.23" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.1" + postcss-nested "^6.0.1" + postcss-selector-parser "^6.0.11" + resolve "^1.22.2" + sucrase "^3.32.0" + +terser@^5.30.3: + version "5.30.3" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.3.tgz#f1bb68ded42408c316b548e3ec2526d7dd03f4d2" + integrity sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +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== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tsconfck@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.3.tgz#d9bda0e87d05b1c360e996c9050473c7e6f8084f" + integrity sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA== + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typescript@^4.0.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + +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" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +vite-tsconfig-paths@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9" + integrity sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + +vite@^5.2.8: + version "5.2.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.8.tgz#a99e09939f1a502992381395ce93efa40a2844aa" + integrity sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA== + dependencies: + esbuild "^0.20.1" + postcss "^8.4.38" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + dependencies: + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + +which-collection@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^2.3.4: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed" + integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.21.4: + version "3.22.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" + integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== diff --git a/bskyweb/.gitignore b/bskyweb/.gitignore index 1d945e1dab..fad122a280 100644 --- a/bskyweb/.gitignore +++ b/bskyweb/.gitignore @@ -3,12 +3,22 @@ test-coverage.out # Don't check in the binary. /bskyweb +/embedr # Don't accidentally commit JS-generated code static/js/*.js static/js/*.map static/js/*.js.LICENSE.txt +static/js/empty.txt templates/scripts.html +templates/*-embed.html +static/embed/*.html +static/embed/assets/*.js +static/embed/assets/*.css +embedr-static/post-*.js +embedr-static/post-*.css +embedr-static/index-*.js +embedr-static/polyfills-*.js # Don't ignore this file !.gitignore diff --git a/bskyweb/Makefile b/bskyweb/Makefile index 6f979fa849..bb2da525fe 100644 --- a/bskyweb/Makefile +++ b/bskyweb/Makefile @@ -14,6 +14,7 @@ help: ## Print info about all commands .PHONY: build build: ## Build all executables go build ./cmd/bskyweb + go build ./cmd/embedr .PHONY: test test: ## Run all tests @@ -43,3 +44,7 @@ check: ## Compile everything, checking syntax (does not output binaries) .PHONY: run-dev-bskyweb run-dev-bskyweb: .env ## Runs 'bskyweb' for local dev GOLOG_LOG_LEVEL=info go run ./cmd/bskyweb serve + +.PHONY: run-dev-embedr +run-dev-embedr: .env ## Runs 'embedr' for local dev + GOLOG_LOG_LEVEL=info go run ./cmd/embedr serve diff --git a/bskyweb/README.embed.md b/bskyweb/README.embed.md new file mode 100644 index 0000000000..8f19ef0226 --- /dev/null +++ b/bskyweb/README.embed.md @@ -0,0 +1,52 @@ + +## oEmbed + + + +* URL scheme: `https://bsky.app/profile/*/post/*` +* API endpoint: `https://embed.bsky.app/oembed` + +Request params: + +- `url` (required): support both AT-URI and bsky.app URL +- `maxwidth` (optional): [220..550], 325 is default +- `maxheight` (not supported!) +- `format` (optional): only `json` supported + +Response format: + +- `type` (required): "rich" +- `version` (required): "1.0" +- `author_name` (optional): display name +- `author_url` (optional): profile URL +- `provider_name` (optional): "Bluesky Social" +- `provider_url` (optional): "https://bsky.app" +- `cache_age` (optional, integer seconds): 86400 (24 hours) (?) +- `width` (required): ? +- `height` (required): ? + +Not used: + +- title (optional): A text title, describing the resource. +- thumbnail_url (optional): A URL to a thumbnail image representing the resource. The thumbnail must respect any maxwidth and maxheight parameters. If this parameter is present, thumbnail_width and thumbnail_height must also be present. +- thumbnail_width (optional): The width of the optional thumbnail. If this parameter is present, thumbnail_url and thumbnail_height must also be present. +- thumbnail_height (optional): The height of the optional thumbnail. If this parameter is present, thumbnail_url and thumbnail_width must also be present. + +Only `json` is supported; `xml` is a 501. + +``` + +``` + + +## iframe URL + +`https://embed.bsky.app/embed//app.bsky.feed.post/` +`https://embed.bsky.app/static/embed.js` + +``` +
+

{{ post-text }}

+ — US Department of the Interior (@Interior) May 5, 2014 +
+``` diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 54a3925c6d..54580d6431 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -170,6 +170,9 @@ func serve(cctx *cli.Context) error { // home e.GET("/", server.WebHome) + // download + e.GET("/download", server.Download) + // generic routes e.GET("/hashtag/:tag", server.WebGeneric) e.GET("/search", server.WebGeneric) @@ -187,6 +190,7 @@ func serve(cctx *cli.Context) error { e.GET("/settings/saved-feeds", server.WebGeneric) e.GET("/settings/threads", server.WebGeneric) e.GET("/settings/external-embeds", server.WebGeneric) + e.GET("/settings/accessibility", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/debug-mod", server.WebGeneric) e.GET("/sys/log", server.WebGeneric) @@ -196,6 +200,8 @@ func serve(cctx *cli.Context) error { e.GET("/support/community-guidelines", server.WebGeneric) e.GET("/support/copyright", server.WebGeneric) e.GET("/intent/compose", server.WebGeneric) + e.GET("/messages", server.WebGeneric) + e.GET("/messages/:conversation", server.WebGeneric) // profile endpoints; only first populates info e.GET("/profile/:handleOrDID", server.WebProfile) @@ -271,6 +277,20 @@ func (srv *Server) errorHandler(err error, c echo.Context) { c.Render(code, "error.html", data) } +// Handler for redirecting to the download page. +func (srv *Server) Download(c echo.Context) error { + ua := c.Request().UserAgent() + if strings.Contains(ua, "Android") { + return c.Redirect(http.StatusFound, "https://play.google.com/store/apps/details?id=xyz.blueskyweb.app") + } + + if strings.Contains(ua, "iPhone") || strings.Contains(ua, "iPad") || strings.Contains(ua, "iPod") { + return c.Redirect(http.StatusFound, "https://apps.apple.com/tr/app/bluesky-social/id6444370199") + } + + return c.Redirect(http.StatusFound, "/") +} + // handler for endpoint that have no specific server-side handling func (srv *Server) WebGeneric(c echo.Context) error { data := pongo2.Context{} diff --git a/bskyweb/cmd/embedr/.gitignore b/bskyweb/cmd/embedr/.gitignore new file mode 100644 index 0000000000..c810652a10 --- /dev/null +++ b/bskyweb/cmd/embedr/.gitignore @@ -0,0 +1 @@ +/bskyweb diff --git a/bskyweb/cmd/embedr/handlers.go b/bskyweb/cmd/embedr/handlers.go new file mode 100644 index 0000000000..a3767eeca9 --- /dev/null +++ b/bskyweb/cmd/embedr/handlers.go @@ -0,0 +1,213 @@ +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + appbsky "github.com/bluesky-social/indigo/api/bsky" + "github.com/bluesky-social/indigo/atproto/syntax" + + "github.com/labstack/echo/v4" +) + +var ErrPostNotFound = errors.New("post not found") +var ErrPostNotPublic = errors.New("post is not publicly accessible") + +func (srv *Server) getBlueskyPost(ctx context.Context, did syntax.DID, rkey syntax.RecordKey) (*appbsky.FeedDefs_PostView, error) { + + // fetch the post post (with extra context) + uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey) + tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 0, uri) + if err != nil { + log.Warnf("failed to fetch post: %s\t%v", uri, err) + // TODO: detect 404, specifically? + return nil, ErrPostNotFound + } + + if tpv.Thread.FeedDefs_BlockedPost != nil { + return nil, ErrPostNotPublic + } else if tpv.Thread.FeedDefs_ThreadViewPost.Post == nil { + return nil, ErrPostNotFound + } + + postView := tpv.Thread.FeedDefs_ThreadViewPost.Post + for _, label := range postView.Author.Labels { + if label.Src == postView.Author.Did && label.Val == "!no-unauthenticated" { + return nil, ErrPostNotPublic + } + } + return postView, nil +} + +func (srv *Server) WebHome(c echo.Context) error { + return c.Render(http.StatusOK, "home.html", nil) +} + +type OEmbedResponse struct { + Type string `json:"type"` + Version string `json:"version"` + AuthorName string `json:"author_name,omitempty"` + AuthorURL string `json:"author_url,omitempty"` + ProviderName string `json:"provider_url,omitempty"` + CacheAge int `json:"cache_age,omitempty"` + Width *int `json:"width"` + Height *int `json:"height"` + HTML string `json:"html,omitempty"` +} + +func (srv *Server) parseBlueskyURL(ctx context.Context, raw string) (*syntax.ATURI, error) { + + if raw == "" { + return nil, fmt.Errorf("empty url") + } + + // first try simple AT-URI + uri, err := syntax.ParseATURI(raw) + if nil == err { + return &uri, nil + } + + // then try bsky.app post URL + u, err := url.Parse(raw) + if err != nil { + return nil, err + } + if u.Hostname() != "bsky.app" { + return nil, fmt.Errorf("only bsky.app URLs currently supported") + } + pathParts := strings.Split(u.Path, "/") // NOTE: pathParts[0] will be empty string + if len(pathParts) != 5 || pathParts[1] != "profile" || pathParts[3] != "post" { + return nil, fmt.Errorf("only bsky.app post URLs currently supported") + } + atid, err := syntax.ParseAtIdentifier(pathParts[2]) + if err != nil { + return nil, err + } + rkey, err := syntax.ParseRecordKey(pathParts[4]) + if err != nil { + return nil, err + } + var did syntax.DID + if atid.IsHandle() { + ident, err := srv.dir.Lookup(ctx, *atid) + if err != nil { + return nil, err + } + did = ident.DID + } else { + did, err = atid.AsDID() + if err != nil { + return nil, err + } + } + + // TODO: don't really need to re-parse here, if we had test coverage + aturi, err := syntax.ParseATURI(fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)) + if err != nil { + return nil, err + } else { + return &aturi, nil + } +} + +func (srv *Server) WebOEmbed(c echo.Context) error { + formatParam := c.QueryParam("format") + if formatParam != "" && formatParam != "json" { + return c.String(http.StatusNotImplemented, "Unsupported oEmbed format: "+formatParam) + } + + // TODO: do we actually do something with width? + width := 600 + maxWidthParam := c.QueryParam("maxwidth") + if maxWidthParam != "" { + maxWidthInt, err := strconv.Atoi(maxWidthParam) + if err != nil { + return c.String(http.StatusBadRequest, "Invalid maxwidth (expected integer)") + } + if maxWidthInt < 220 { + width = 220 + } else if maxWidthInt > 600 { + width = 600 + } else { + width = maxWidthInt + } + } + // NOTE: maxheight ignored + + aturi, err := srv.parseBlueskyURL(c.Request().Context(), c.QueryParam("url")) + if err != nil { + return c.String(http.StatusBadRequest, fmt.Sprintf("Expected 'url' to be bsky.app URL or AT-URI: %v", err)) + } + if aturi.Collection() != syntax.NSID("app.bsky.feed.post") { + return c.String(http.StatusNotImplemented, "Only posts (app.bsky.feed.post records) can be embedded currently") + } + did, err := aturi.Authority().AsDID() + if err != nil { + return err + } + + post, err := srv.getBlueskyPost(c.Request().Context(), did, aturi.RecordKey()) + if err == ErrPostNotFound { + return c.String(http.StatusNotFound, fmt.Sprintf("%v", err)) + } else if err == ErrPostNotPublic { + return c.String(http.StatusForbidden, fmt.Sprintf("%v", err)) + } else if err != nil { + return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) + } + + html, err := srv.postEmbedHTML(post) + if err != nil { + return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) + } + data := OEmbedResponse{ + Type: "rich", + Version: "1.0", + AuthorName: "@" + post.Author.Handle, + AuthorURL: fmt.Sprintf("https://bsky.app/profile/%s", post.Author.Handle), + ProviderName: "Bluesky Social", + CacheAge: 86400, + Width: &width, + Height: nil, + HTML: html, + } + if post.Author.DisplayName != nil { + data.AuthorName = fmt.Sprintf("%s (@%s)", *post.Author.DisplayName, post.Author.Handle) + } + return c.JSON(http.StatusOK, data) +} + +func (srv *Server) WebPostEmbed(c echo.Context) error { + + // sanity check arguments. don't 4xx, just let app handle if not expected format + rkeyParam := c.Param("rkey") + rkey, err := syntax.ParseRecordKey(rkeyParam) + if err != nil { + return c.String(http.StatusBadRequest, fmt.Sprintf("Invalid RecordKey: %v", err)) + } + didParam := c.Param("did") + did, err := syntax.ParseDID(didParam) + if err != nil { + return c.String(http.StatusBadRequest, fmt.Sprintf("Invalid DID: %v", err)) + } + _ = rkey + _ = did + + // NOTE: this request was't really necessary; the JS will do the same fetch + /* + postView, err := srv.getBlueskyPost(ctx, did, rkey) + if err == ErrPostNotFound { + return c.String(http.StatusNotFound, fmt.Sprintf("%v", err)) + } else if err == ErrPostNotPublic { + return c.String(http.StatusForbidden, fmt.Sprintf("%v", err)) + } else if err != nil { + return c.String(http.StatusInternalServerError, fmt.Sprintf("%v", err)) + } + */ + + return c.Render(http.StatusOK, "postEmbed.html", nil) +} diff --git a/bskyweb/cmd/embedr/main.go b/bskyweb/cmd/embedr/main.go new file mode 100644 index 0000000000..9f75ed69af --- /dev/null +++ b/bskyweb/cmd/embedr/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "os" + + _ "github.com/joho/godotenv/autoload" + + logging "github.com/ipfs/go-log" + "github.com/urfave/cli/v2" +) + +var log = logging.Logger("embedr") + +func init() { + logging.SetAllLoggers(logging.LevelDebug) + //logging.SetAllLoggers(logging.LevelWarn) +} + +func main() { + run(os.Args) +} + +func run(args []string) { + + app := cli.App{ + Name: "embedr", + Usage: "web server for embed.bsky.app post embeds", + } + + app.Commands = []*cli.Command{ + &cli.Command{ + Name: "serve", + Usage: "run the server", + Action: serve, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "appview-host", + Usage: "method, hostname, and port of PDS instance", + Value: "https://public.api.bsky.app", + EnvVars: []string{"ATP_APPVIEW_HOST"}, + }, + &cli.StringFlag{ + Name: "http-address", + Usage: "Specify the local IP/port to bind to", + Required: false, + Value: ":8100", + EnvVars: []string{"HTTP_ADDRESS"}, + }, + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug mode", + Value: false, + Required: false, + EnvVars: []string{"DEBUG"}, + }, + }, + }, + } + app.RunAndExitOnError() +} diff --git a/bskyweb/cmd/embedr/render.go b/bskyweb/cmd/embedr/render.go new file mode 100644 index 0000000000..cc8f0759a0 --- /dev/null +++ b/bskyweb/cmd/embedr/render.go @@ -0,0 +1,16 @@ +package main + +import ( + "html/template" + "io" + + "github.com/labstack/echo/v4" +) + +type Template struct { + templates *template.Template +} + +func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} diff --git a/bskyweb/cmd/embedr/server.go b/bskyweb/cmd/embedr/server.go new file mode 100644 index 0000000000..904b4df9a2 --- /dev/null +++ b/bskyweb/cmd/embedr/server.go @@ -0,0 +1,236 @@ +package main + +import ( + "context" + "errors" + "fmt" + "html/template" + "io/fs" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/bluesky-social/indigo/atproto/identity" + "github.com/bluesky-social/indigo/util/cliutil" + "github.com/bluesky-social/indigo/xrpc" + "github.com/bluesky-social/social-app/bskyweb" + + "github.com/klauspost/compress/gzhttp" + "github.com/klauspost/compress/gzip" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/urfave/cli/v2" +) + +type Server struct { + echo *echo.Echo + httpd *http.Server + xrpcc *xrpc.Client + dir identity.Directory +} + +func serve(cctx *cli.Context) error { + debug := cctx.Bool("debug") + httpAddress := cctx.String("http-address") + appviewHost := cctx.String("appview-host") + + // Echo + e := echo.New() + + // create a new session (no auth) + xrpcc := &xrpc.Client{ + Client: cliutil.NewHttpClient(), + Host: appviewHost, + } + + // httpd + var ( + httpTimeout = 2 * time.Minute + httpMaxHeaderBytes = 2 * (1024 * 1024) + gzipMinSizeBytes = 1024 * 2 + gzipCompressionLevel = gzip.BestSpeed + gzipExceptMIMETypes = []string{"image/png"} + ) + + // Wrap the server handler in a gzip handler to compress larger responses. + gzipHandler, err := gzhttp.NewWrapper( + gzhttp.MinSize(gzipMinSizeBytes), + gzhttp.CompressionLevel(gzipCompressionLevel), + gzhttp.ExceptContentTypes(gzipExceptMIMETypes), + ) + if err != nil { + return err + } + + // + // server + // + server := &Server{ + echo: e, + xrpcc: xrpcc, + dir: identity.DefaultDirectory(), + } + + // Create the HTTP server. + server.httpd = &http.Server{ + Handler: gzipHandler(server), + Addr: httpAddress, + WriteTimeout: httpTimeout, + ReadTimeout: httpTimeout, + MaxHeaderBytes: httpMaxHeaderBytes, + } + + e.HideBanner = true + + tmpl := &Template{ + templates: template.Must(template.ParseFS(bskyweb.EmbedrTemplateFS, "embedr-templates/*.html")), + } + e.Renderer = tmpl + e.HTTPErrorHandler = server.errorHandler + + e.IPExtractor = echo.ExtractIPFromXFFHeader() + + // SECURITY: Do not modify without due consideration. + e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + ContentTypeNosniff: "nosniff", + // diable XFrameOptions; we're embedding here! + HSTSMaxAge: 31536000, // 365 days + // TODO: + // ContentSecurityPolicy + // XSSProtection + })) + e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ + // Don't log requests for static content. + Skipper: func(c echo.Context) bool { + return strings.HasPrefix(c.Request().URL.Path, "/static") + }, + })) + e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ + Skipper: middleware.DefaultSkipper, + Store: middleware.NewRateLimiterMemoryStoreWithConfig( + middleware.RateLimiterMemoryStoreConfig{ + Rate: 10, // requests per second + Burst: 30, // allow bursts + ExpiresIn: 3 * time.Minute, // garbage collect entries older than 3 minutes + }, + ), + IdentifierExtractor: func(ctx echo.Context) (string, error) { + id := ctx.RealIP() + return id, nil + }, + DenyHandler: func(c echo.Context, identifier string, err error) error { + return c.String(http.StatusTooManyRequests, "Your request has been rate limited. Please try again later. Contact support@bsky.app if you believe this was a mistake.\n") + }, + })) + + // redirect trailing slash to non-trailing slash. + // all of our current endpoints have no trailing slash. + e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ + RedirectCode: http.StatusFound, + })) + + // + // configure routes + // + // static files + staticHandler := http.FileServer(func() http.FileSystem { + if debug { + log.Debugf("serving static file from the local file system") + return http.FS(os.DirFS("embedr-static")) + } + fsys, err := fs.Sub(bskyweb.EmbedrStaticFS, "embedr-static") + if err != nil { + log.Fatal(err) + } + return http.FS(fsys) + }()) + + e.GET("/robots.txt", echo.WrapHandler(staticHandler)) + e.GET("/ips-v4", echo.WrapHandler(staticHandler)) + e.GET("/ips-v6", echo.WrapHandler(staticHandler)) + e.GET("/.well-known/*", echo.WrapHandler(staticHandler)) + e.GET("/security.txt", func(c echo.Context) error { + return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt") + }) + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + path := c.Request().URL.Path + maxAge := 1 * (60 * 60) // default is 1 hour + + // Cache javascript and images files for 1 week, which works because + // they're always versioned (e.g. /static/js/main.64c14927.js) + if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/images/") { + maxAge = 7 * (60 * 60 * 24) // 1 week + } + + c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge)) + return next(c) + } + }) + + // actual routes + e.GET("/", server.WebHome) + e.GET("/iframe-resize.js", echo.WrapHandler(staticHandler)) + e.GET("/embed.js", echo.WrapHandler(staticHandler)) + e.GET("/oembed", server.WebOEmbed) + e.GET("/embed/:did/app.bsky.feed.post/:rkey", server.WebPostEmbed) + + // Start the server. + log.Infof("starting server address=%s", httpAddress) + go func() { + if err := server.httpd.ListenAndServe(); err != nil { + if !errors.Is(err, http.ErrServerClosed) { + log.Errorf("HTTP server shutting down unexpectedly: %s", err) + } + } + }() + + // Wait for a signal to exit. + log.Info("registering OS exit signal handler") + quit := make(chan struct{}) + exitSignals := make(chan os.Signal, 1) + signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-exitSignals + log.Infof("received OS exit signal: %s", sig) + + // Shut down the HTTP server. + if err := server.Shutdown(); err != nil { + log.Errorf("HTTP server shutdown error: %s", err) + } + + // Trigger the return that causes an exit. + close(quit) + }() + <-quit + log.Infof("graceful shutdown complete") + return nil +} + +func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + srv.echo.ServeHTTP(rw, req) +} + +func (srv *Server) Shutdown() error { + log.Info("shutting down") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + return srv.httpd.Shutdown(ctx) +} + +func (srv *Server) errorHandler(err error, c echo.Context) { + code := http.StatusInternalServerError + if he, ok := err.(*echo.HTTPError); ok { + code = he.Code + } + c.Logger().Error(err) + data := map[string]interface{}{ + "statusCode": code, + } + c.Render(code, "error.html", data) +} diff --git a/bskyweb/cmd/embedr/snippet.go b/bskyweb/cmd/embedr/snippet.go new file mode 100644 index 0000000000..b93acb2cd1 --- /dev/null +++ b/bskyweb/cmd/embedr/snippet.go @@ -0,0 +1,79 @@ +package main + +import ( + "bytes" + "fmt" + "html/template" + + appbsky "github.com/bluesky-social/indigo/api/bsky" + "github.com/bluesky-social/indigo/atproto/syntax" +) + +func (srv *Server) postEmbedHTML(postView *appbsky.FeedDefs_PostView) (string, error) { + // ensure that there isn't an injection from the URI + aturi, err := syntax.ParseATURI(postView.Uri) + if err != nil { + log.Error("bad AT-URI in reponse", "aturi", aturi, "err", err) + return "", err + } + + post, ok := postView.Record.Val.(*appbsky.FeedPost) + if !ok { + log.Error("bad post record value", "err", err) + return "", err + } + + const tpl = `
{{ .PostText }}

{{ .PostAuthor }} {{ .PostIndexedAt }}
` + + t, err := template.New("snippet").Parse(tpl) + if err != nil { + log.Error("template parse error", "err", err) + return "", err + } + + sortAt := postView.IndexedAt + createdAt, err := syntax.ParseDatetime(post.CreatedAt) + if nil == err && createdAt.String() < sortAt { + sortAt = createdAt.String() + } + + var lang string + if len(post.Langs) > 0 { + lang = post.Langs[0] + } + var authorName string + if postView.Author.DisplayName != nil { + authorName = fmt.Sprintf("%s (@%s)", *postView.Author.DisplayName, postView.Author.Handle) + } else { + authorName = fmt.Sprintf("@%s", postView.Author.Handle) + } + data := struct { + PostURI template.URL + PostCID string + PostLang string + PostText string + PostAuthor string + PostIndexedAt string + ProfileURL template.URL + PostURL template.URL + WidgetURL template.URL + }{ + PostURI: template.URL(postView.Uri), + PostCID: postView.Cid, + PostLang: lang, + PostText: post.Text, + PostAuthor: authorName, + PostIndexedAt: sortAt, + ProfileURL: template.URL(fmt.Sprintf("https://bsky.app/profile/%s?ref_src=embed", aturi.Authority())), + PostURL: template.URL(fmt.Sprintf("https://bsky.app/profile/%s/post/%s?ref_src=embed", aturi.Authority(), aturi.RecordKey())), + WidgetURL: template.URL("https://embed.bsky.app/static/embed.js"), + } + + var buf bytes.Buffer + err = t.Execute(&buf, data) + if err != nil { + log.Error("template parse error", "err", err) + return "", err + } + return buf.String(), nil +} diff --git a/bskyweb/embedr-static/.well-known/security.txt b/bskyweb/embedr-static/.well-known/security.txt new file mode 100644 index 0000000000..8173cb72d6 --- /dev/null +++ b/bskyweb/embedr-static/.well-known/security.txt @@ -0,0 +1,4 @@ +Contact: mailto:security@bsky.app +Preferred-Languages: en +Canonical: https://bsky.app/.well-known/security.txt +Acknowledgements: https://github.com/bluesky-social/atproto/blob/main/CONTRIBUTORS.md diff --git a/bskyweb/embedr-static/embed.js b/bskyweb/embedr-static/embed.js new file mode 100644 index 0000000000..15964a76c3 --- /dev/null +++ b/bskyweb/embedr-static/embed.js @@ -0,0 +1 @@ +/* embed javascript widget will go here */ diff --git a/bskyweb/embedr-static/favicon-16x16.png b/bskyweb/embedr-static/favicon-16x16.png new file mode 100644 index 0000000000..ea256e0569 Binary files /dev/null and b/bskyweb/embedr-static/favicon-16x16.png differ diff --git a/bskyweb/embedr-static/favicon-32x32.png b/bskyweb/embedr-static/favicon-32x32.png new file mode 100644 index 0000000000..a5ca7eed1e Binary files /dev/null and b/bskyweb/embedr-static/favicon-32x32.png differ diff --git a/bskyweb/embedr-static/favicon.png b/bskyweb/embedr-static/favicon.png new file mode 100644 index 0000000000..ddf55f4c81 Binary files /dev/null and b/bskyweb/embedr-static/favicon.png differ diff --git a/bskyweb/embedr-static/iframe-resize.js b/bskyweb/embedr-static/iframe-resize.js new file mode 100644 index 0000000000..6bf2793df5 --- /dev/null +++ b/bskyweb/embedr-static/iframe-resize.js @@ -0,0 +1 @@ +/* script to resize embed ifame would go here? */ diff --git a/bskyweb/embedr-static/ips-v4 b/bskyweb/embedr-static/ips-v4 new file mode 100644 index 0000000000..087996ef9a --- /dev/null +++ b/bskyweb/embedr-static/ips-v4 @@ -0,0 +1,30 @@ +13.59.225.103/32 +3.18.47.21/32 +18.191.104.94/32 +3.129.134.255/32 +3.129.237.113/32 +3.138.56.230/32 +44.218.10.163/32 +54.89.116.251/32 +44.217.166.202/32 +54.208.221.149/32 +54.166.110.54/32 +54.208.146.65/32 +3.129.234.15/32 +3.138.168.48/32 +3.23.53.192/32 +52.14.89.53/32 +3.18.126.246/32 +3.136.69.4/32 +3.22.137.152/32 +3.132.247.113/32 +3.141.186.104/32 +18.222.43.214/32 +3.14.35.197/32 +3.23.182.70/32 +18.224.144.69/32 +3.129.98.29/32 +3.130.134.20/32 +3.17.197.213/32 +18.223.234.21/32 +3.20.248.177/32 diff --git a/bskyweb/embedr-static/ips-v6 b/bskyweb/embedr-static/ips-v6 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bskyweb/embedr-static/robots.txt b/bskyweb/embedr-static/robots.txt new file mode 100644 index 0000000000..4f8510d18d --- /dev/null +++ b/bskyweb/embedr-static/robots.txt @@ -0,0 +1,9 @@ +# Hello Friends! +# If you are considering bulk or automated crawling, you may want to look in +# to our protocol (API), including a firehose of updates. See: https://atproto.com/ + +# By default, may crawl anything on this domain. HTTP 429 ("backoff") status +# codes are used for rate-limiting. Up to a handful concurrent requests should +# be ok. +User-Agent: * +Allow: / diff --git a/bskyweb/embedr-templates/error.html b/bskyweb/embedr-templates/error.html new file mode 100644 index 0000000000..5aa04c83bf --- /dev/null +++ b/bskyweb/embedr-templates/error.html @@ -0,0 +1 @@ +placeholder! diff --git a/bskyweb/embedr-templates/home.html b/bskyweb/embedr-templates/home.html new file mode 100644 index 0000000000..f938c32d6e --- /dev/null +++ b/bskyweb/embedr-templates/home.html @@ -0,0 +1,8 @@ + + + + +

embed.bsky.app homepage

+

could redirect to bsky.app? or show a "create embed" widget? + + diff --git a/bskyweb/embedr-templates/oembed.html b/bskyweb/embedr-templates/oembed.html new file mode 100644 index 0000000000..646f0a482c --- /dev/null +++ b/bskyweb/embedr-templates/oembed.html @@ -0,0 +1 @@ +oembed JSON response will go here diff --git a/bskyweb/embedr-templates/postEmbed.html b/bskyweb/embedr-templates/postEmbed.html new file mode 100644 index 0000000000..6329b3a199 --- /dev/null +++ b/bskyweb/embedr-templates/postEmbed.html @@ -0,0 +1 @@ +embed post HTML will go here diff --git a/bskyweb/static.go b/bskyweb/static.go index a67d189f57..38adb83335 100644 --- a/bskyweb/static.go +++ b/bskyweb/static.go @@ -4,3 +4,6 @@ import "embed" //go:embed static/* var StaticFS embed.FS + +//go:embed embedr-static/* +var EmbedrStaticFS embed.FS diff --git a/bskyweb/templates.go b/bskyweb/templates.go index ce3fa29af7..a66965aba4 100644 --- a/bskyweb/templates.go +++ b/bskyweb/templates.go @@ -4,3 +4,6 @@ import "embed" //go:embed templates/* var TemplateFS embed.FS + +//go:embed embedr-templates/* +var EmbedrTemplateFS embed.FS diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 34e5901069..cb0cea24b4 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -235,6 +235,17 @@ inset:0; animation: rotate 500ms linear infinite; } + + @keyframes avatarHoverFadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + @keyframes avatarHoverFadeOut { + from { opacity: 1; } + to { opacity: 0; } + } + {% include "scripts.html" %} diff --git a/bskyweb/templates/post.html b/bskyweb/templates/post.html index af6b768b38..d1fbea0ac3 100644 --- a/bskyweb/templates/post.html +++ b/bskyweb/templates/post.html @@ -36,6 +36,8 @@ + + {% endif -%} {%- endblock %} diff --git a/docs/build.md b/docs/build.md index d1f9f93b5a..deab91a5ba 100644 --- a/docs/build.md +++ b/docs/build.md @@ -2,7 +2,8 @@ ## App Build -- Set up your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup). +- Set up your environment [using the expo instructions](https://docs.expo.dev/guides/local-app-development/). + - make sure that the JAVA_HOME points to the zulu-17 directory in your `.zshrc` or `.bashrc` file: `export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home`. DO NOT use another JDK or you will encounter build errors. - If you're running macOS, make sure you are running the correct versions of Ruby and Cocoapods: - Check if you've installed Cocoapods through `homebrew`. If you have, remove it: - `brew info cocoapods` @@ -14,7 +15,7 @@ - `rbenv global 2.7.6` - Add `eval "$(rbenv init - zsh)"` to your `~/.zshrc` - From inside the project directory: - - `bundler install` + - `bundler install` (this will install Cocoapods) - Setup your environment [for e2e testing using detox](https://wix.github.io/Detox/docs/introduction/getting-started): - `yarn global add detox-cli` - `brew tap wix/brew` @@ -27,7 +28,7 @@ - `git clone git@github.com:bluesky-social/atproto.git` - `cd atproto` - `brew install pnpm` - - `brew install jq` + - optional: `brew install jq` - `pnpm i` - `pnpm build` - Start the docker daemon (on MacOS this entails starting the Docker Desktop app) @@ -38,11 +39,22 @@ - Xcode must be installed for this to run. - A simulator must be preconfigured in Xcode settings. - if no iOS versions are available, install the iOS runtime at `Xcode > Settings > Platforms`. + - if the simulator download keeps failing you can download it from the developer website. + - [Apple Developer](https://developer.apple.com/download/all/?q=Simulator%20Runtime) + - `xcode-select -s /Applications/Xcode.app` + - `xcodebuild -runFirstLaunch` + - `xcrun simctl runtime add "~/Downloads/iOS_17.4_Simulator_Runtime.dmg"` (adapt the path to the downloaded file) - In addition, ensure Xcode Command Line Tools are installed using `xcode-select --install`. - - Pods must be installed: - - From the project directory root: `cd ios && pod install`. - Expo will require you to configure Xcode Signing. Follow the linked instructions. Error messages in Xcode related to the signing process can be safely ignored when installing on the iOS Simulator; Expo merely requires the profile to exist in order to install the app on the Simulator. + - Make sure you do have a certificate: open Xcode > Settings > Accounts > (sign-in) > Manage Certificates > + > Apple Development > Done. + - If you still encounter issues, try `rm -rf ios` before trying to build again (`yarn ios`) - Android: `yarn android` + - Install "Android Studio" + - Make sure you have the Android SDK installed (Android Studio > Tools > Android SDK). + - In "SDK Platforms": "Android x" (where x is Android's current version). + - In "SDK Tools": "Android SDK Build-Tools" and "Android Emulator" are required. + - Add `export ANDROID_HOME=/Users//Library/Android/sdk` to your `.zshrc` or `.bashrc` (and restart your terminal). + - Setup an emulator (Android Studio > Tools > Device Manager). - Web: `yarn web` - If you are cloning or forking this repo as an open-source developer, please check the tips below as well - Run e2e tests @@ -83,11 +95,10 @@ To run the build with Go, use staging credentials, your own, or any other accoun ``` cd social-app yarn && yarn build-web -cp ./web-build/static/js/*.* bskyweb/static/js/ cd bskyweb/ go mod tidy go build -v -tags timetzdata -o bskyweb ./cmd/bskyweb -./bskyweb serve --pds-host=https://staging.bsky.dev --handle= --password= +./bskyweb serve --appview-host=https://public.api.bsky.app ``` On build success, access the application at [http://localhost:8100/](http://localhost:8100/). Subsequent changes require re-running the above steps in order to be reflected. diff --git a/eslint/use-typed-gates.js b/eslint/use-typed-gates.js index 3625a7da37..b245072ba5 100644 --- a/eslint/use-typed-gates.js +++ b/eslint/use-typed-gates.js @@ -18,14 +18,14 @@ exports.create = function create(context) { return } const source = node.parent.source.value - if (source.startsWith('.') || source.startsWith('#')) { - return + if (source.startsWith('statsig') || source.startsWith('@statsig')) { + context.report({ + node, + message: + "Use useGate() from '#/lib/statsig/statsig' instead of the one on npm.", + }) } - context.report({ - node, - message: - "Use useGate() from '#/lib/statsig/statsig' instead of the one on npm.", - }) + // TODO: Verify gate() call results aren't stored in variables. }, } } diff --git a/metro.config.js b/metro.config.js index a49d95f9aa..80d2e34baf 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,7 +1,47 @@ // Learn more https://docs.expo.io/guides/customizing-metro +const path = require('path') const {getDefaultConfig} = require('expo/metro-config') const cfg = getDefaultConfig(__dirname) +if (process.env.ATPROTO_ROOT) { + const atprotoRoot = path.resolve(process.cwd(), process.env.ATPROTO_ROOT) + + // Watch folders are used as roots for the virtual file system. Any file that + // needs to be resolved by the metro bundler must be within one of the watch + // folders. Since we will be resolving dependencies from the atproto packages, + // we need to add the atproto root to the watch folders so that the + cfg.watchFolders ||= [] + cfg.watchFolders.push(atprotoRoot) + + const resolveRequest = cfg.resolver.resolveRequest + cfg.resolver.resolveRequest = (context, moduleName, platform) => { + // Alias @atproto/* modules to the corresponding package in the atproto root + if (moduleName.startsWith('@atproto/')) { + const [, packageName] = moduleName.split('/', 2) + const packagePath = path.join(atprotoRoot, 'packages', packageName) + return context.resolveRequest(context, packagePath, platform) + } + + // Polyfills are added by the build process and are not actual dependencies + // of the @atproto/* packages. Resolve those from here. + if ( + moduleName.startsWith('@babel/') && + context.originModulePath.startsWith(atprotoRoot) + ) { + return { + type: 'sourceFile', + filePath: require.resolve(moduleName), + } + } + + return (resolveRequest || context.resolveRequest)( + context, + moduleName, + platform, + ) + } +} + cfg.resolver.sourceExts = process.env.RN_SRC_EXT ? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts) : cfg.resolver.sourceExts diff --git a/modules/expo-bluesky-gif-view/android/build.gradle b/modules/expo-bluesky-gif-view/android/build.gradle new file mode 100644 index 0000000000..c209a35aec --- /dev/null +++ b/modules/expo-bluesky-gif-view/android/build.gradle @@ -0,0 +1,98 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'maven-publish' + +group = 'expo.modules.blueskygifview' +version = '0.5.0' + +buildscript { + def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") + if (expoModulesCorePlugin.exists()) { + apply from: expoModulesCorePlugin + applyKotlinExpoModulesCorePlugin() + } + + // Simple helper that allows the root project to override versions declared by this library. + ext.safeExtGet = { prop, fallback -> + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback + } + + // Ensures backward compatibility + ext.getKotlinVersion = { + if (ext.has("kotlinVersion")) { + ext.kotlinVersion() + } else { + ext.safeExtGet("kotlinVersion", "1.8.10") + } + } + + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}") + } +} + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + } + } + repositories { + maven { + url = mavenLocal().url + } + } + } +} + +android { + compileSdkVersion safeExtGet("compileSdkVersion", 33) + + def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION + if (agpVersion.tokenize('.')[0].toInteger() < 8) { + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.majorVersion + } + } + + namespace "expo.modules.blueskygifview" + defaultConfig { + minSdkVersion safeExtGet("minSdkVersion", 21) + targetSdkVersion safeExtGet("targetSdkVersion", 34) + versionCode 1 + versionName "0.5.0" + } + lintOptions { + abortOnError false + } + publishing { + singleVariant("release") { + withSourcesJar() + } + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + def GLIDE_VERSION = "4.13.2" + + implementation project(':expo-modules-core') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" + + // Keep glide version up to date with expo-image so that we don't have duplicate deps + implementation 'com.github.bumptech.glide:glide:4.13.2' +} diff --git a/modules/expo-bluesky-gif-view/android/src/main/AndroidManifest.xml b/modules/expo-bluesky-gif-view/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bdae66c8f5 --- /dev/null +++ b/modules/expo-bluesky-gif-view/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt new file mode 100644 index 0000000000..5d20848453 --- /dev/null +++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt @@ -0,0 +1,37 @@ +package expo.modules.blueskygifview + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Animatable +import androidx.appcompat.widget.AppCompatImageView + +class AppCompatImageViewExtended(context: Context, private val parent: GifView): AppCompatImageView(context) { + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (this.drawable is Animatable) { + if (!parent.isLoaded) { + parent.isLoaded = true + parent.firePlayerStateChange() + } + + if (!parent.isPlaying) { + this.pause() + } + } + } + + fun pause() { + val drawable = this.drawable + if (drawable is Animatable) { + drawable.stop() + } + } + + fun play() { + val drawable = this.drawable + if (drawable is Animatable) { + drawable.start() + } + } +} \ No newline at end of file diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt new file mode 100644 index 0000000000..625e1d45f9 --- /dev/null +++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt @@ -0,0 +1,54 @@ +package expo.modules.blueskygifview + +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoBlueskyGifViewModule : Module() { + override fun definition() = ModuleDefinition { + Name("ExpoBlueskyGifView") + + AsyncFunction("prefetchAsync") { sources: List -> + val activity = appContext.currentActivity ?: return@AsyncFunction + val glide = Glide.with(activity) + + sources.forEach { source -> + glide + .download(source) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .submit() + } + } + + View(GifView::class) { + Events( + "onPlayerStateChange" + ) + + Prop("source") { view: GifView, source: String -> + view.source = source + } + + Prop("placeholderSource") { view: GifView, source: String -> + view.placeholderSource = source + } + + Prop("autoplay") { view: GifView, autoplay: Boolean -> + view.autoplay = autoplay + } + + AsyncFunction("playAsync") { view: GifView -> + view.play() + } + + AsyncFunction("pauseAsync") { view: GifView -> + view.pause() + } + + AsyncFunction("toggleAsync") { view: GifView -> + view.toggle() + } + } + } +} diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt new file mode 100644 index 0000000000..be5830df7a --- /dev/null +++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt @@ -0,0 +1,180 @@ +package expo.modules.blueskygifview + + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Animatable +import android.graphics.drawable.Drawable +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import expo.modules.kotlin.AppContext +import expo.modules.kotlin.exception.Exceptions +import expo.modules.kotlin.viewevent.EventDispatcher +import expo.modules.kotlin.views.ExpoView + +class GifView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { + // Events + private val onPlayerStateChange by EventDispatcher() + + // Glide + private val activity = appContext.currentActivity ?: throw Exceptions.MissingActivity() + private val glide = Glide.with(activity) + val imageView = AppCompatImageViewExtended(context, this) + var isPlaying = true + var isLoaded = false + + // Requests + private var placeholderRequest: Target? = null + private var webpRequest: Target? = null + + // Props + var placeholderSource: String? = null + var source: String? = null + var autoplay: Boolean = true + set(value) { + field = value + + if (value) { + this.play() + } else { + this.pause() + } + } + + + // + + init { + this.setBackgroundColor(Color.TRANSPARENT) + + this.imageView.setBackgroundColor(Color.TRANSPARENT) + this.imageView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + + this.addView(this.imageView) + } + + override fun onAttachedToWindow() { + if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) { + this.load() + } else if (this.isPlaying) { + this.imageView.play() + } + super.onAttachedToWindow() + } + + override fun onDetachedFromWindow() { + this.imageView.pause() + super.onDetachedFromWindow() + } + + // + + // + + private fun load() { + if (placeholderSource == null || source == null) { + return + } + + this.webpRequest = glide.load(source) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .skipMemoryCache(false) + .listener(object: RequestListener { + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: com.bumptech.glide.load.DataSource?, + isFirstResource: Boolean + ): Boolean { + if (placeholderRequest != null) { + glide.clear(placeholderRequest) + } + return false + } + + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + return true + } + }) + .into(this.imageView) + + if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) { + this.placeholderRequest = glide.load(placeholderSource) + .diskCacheStrategy(DiskCacheStrategy.DATA) + // Let's not bloat the memory cache with placeholders + .skipMemoryCache(true) + .listener(object: RequestListener { + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: com.bumptech.glide.load.DataSource?, + isFirstResource: Boolean + ): Boolean { + // Incase this request finishes after the webp, let's just not set + // the drawable. This shouldn't happen because the request should get cancelled + if (imageView.drawable == null) { + imageView.setImageDrawable(resource) + } + return true + } + + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + return true + } + }) + .submit() + } + } + + // + + // + + fun play() { + this.imageView.play() + this.isPlaying = true + this.firePlayerStateChange() + } + + fun pause() { + this.imageView.pause() + this.isPlaying = false + this.firePlayerStateChange() + } + + fun toggle() { + if (this.isPlaying) { + this.pause() + } else { + this.play() + } + } + + // + + // + + fun firePlayerStateChange() { + onPlayerStateChange(mapOf( + "isPlaying" to this.isPlaying, + "isLoaded" to this.isLoaded, + )) + } + + // +} diff --git a/modules/expo-bluesky-gif-view/expo-module.config.json b/modules/expo-bluesky-gif-view/expo-module.config.json new file mode 100644 index 0000000000..0756c8e24c --- /dev/null +++ b/modules/expo-bluesky-gif-view/expo-module.config.json @@ -0,0 +1,9 @@ +{ + "platforms": ["ios", "android", "web"], + "ios": { + "modules": ["ExpoBlueskyGifViewModule"] + }, + "android": { + "modules": ["expo.modules.blueskygifview.ExpoBlueskyGifViewModule"] + } +} diff --git a/modules/expo-bluesky-gif-view/index.ts b/modules/expo-bluesky-gif-view/index.ts new file mode 100644 index 0000000000..0244a54914 --- /dev/null +++ b/modules/expo-bluesky-gif-view/index.ts @@ -0,0 +1 @@ +export {GifView} from './src/GifView' diff --git a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec new file mode 100644 index 0000000000..ddd0877b24 --- /dev/null +++ b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'ExpoBlueskyGifView' + s.version = '1.0.0' + s.summary = 'A simple GIF player for Bluesky' + s.description = 'A simple GIF player for Bluesky' + s.author = '' + s.homepage = 'https://github.com/bluesky-social/social-app' + s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + s.dependency 'SDWebImage', '~> 5.17.0' + s.dependency 'SDWebImageWebPCoder', '~> 0.13.0' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift new file mode 100644 index 0000000000..7c7132290d --- /dev/null +++ b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift @@ -0,0 +1,47 @@ +import ExpoModulesCore +import SDWebImage +import SDWebImageWebPCoder + +public class ExpoBlueskyGifViewModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoBlueskyGifView") + + OnCreate { + SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared) + } + + AsyncFunction("prefetchAsync") { (sources: [URL]) in + SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil) + } + + View(GifView.self) { + Events( + "onPlayerStateChange" + ) + + Prop("source") { (view: GifView, prop: String) in + view.source = prop + } + + Prop("placeholderSource") { (view: GifView, prop: String) in + view.placeholderSource = prop + } + + Prop("autoplay") { (view: GifView, prop: Bool) in + view.autoplay = prop + } + + AsyncFunction("toggleAsync") { (view: GifView) in + view.toggle() + } + + AsyncFunction("playAsync") { (view: GifView) in + view.play() + } + + AsyncFunction("pauseAsync") { (view: GifView) in + view.pause() + } + } + } +} diff --git a/modules/expo-bluesky-gif-view/ios/GifView.swift b/modules/expo-bluesky-gif-view/ios/GifView.swift new file mode 100644 index 0000000000..de722d7a63 --- /dev/null +++ b/modules/expo-bluesky-gif-view/ios/GifView.swift @@ -0,0 +1,185 @@ +import ExpoModulesCore +import SDWebImage +import SDWebImageWebPCoder + +typealias SDWebImageContext = [SDWebImageContextOption: Any] + +public class GifView: ExpoView, AVPlayerViewControllerDelegate { + // Events + private let onPlayerStateChange = EventDispatcher() + + // SDWebImage + private let imageView = SDAnimatedImageView(frame: .zero) + private let imageManager = SDWebImageManager( + cache: SDImageCache.shared, + loader: SDImageLoadersManager.shared + ) + private var isPlaying = true + private var isLoaded = false + + // Requests + private var webpOperation: SDWebImageCombinedOperation? + private var placeholderOperation: SDWebImageCombinedOperation? + + // Props + var source: String? = nil + var placeholderSource: String? = nil + var autoplay = true { + didSet { + if !autoplay { + self.pause() + } else { + self.play() + } + } + } + + // MARK: - Lifecycle + + public required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + self.clipsToBounds = true + + self.imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.imageView.layer.masksToBounds = false + self.imageView.backgroundColor = .clear + self.imageView.contentMode = .scaleToFill + + // We have to explicitly set this to false. If we don't, every time + // the view comes into the viewport, it will start animating again + self.imageView.autoPlayAnimatedImage = false + + self.addSubview(self.imageView) + } + + public override func willMove(toWindow newWindow: UIWindow?) { + if newWindow == nil { + // Don't cancel the placeholder operation, because we really want that to complete for + // when we scroll back up + self.webpOperation?.cancel() + self.placeholderOperation?.cancel() + } else if self.imageView.image == nil { + self.load() + } + } + + // MARK: - Loading + + private func load() { + guard let source = self.source, let placeholderSource = self.placeholderSource else { + return + } + + self.webpOperation?.cancel() + self.placeholderOperation?.cancel() + + // We only need to start an operation for the placeholder if it doesn't exist + // in the cache already. Cache key is by default the absolute URL of the image. + // See: + // https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently + if !SDImageCache.shared.diskImageDataExists(withKey: source), + let url = URL(string: placeholderSource) + { + self.placeholderOperation = imageManager.loadImage( + with: url, + options: [.retryFailed], + context: Util.createContext(), + progress: onProgress(_:_:_:), + completed: onLoaded(_:_:_:_:_:_:) + ) + } + + if let url = URL(string: source) { + self.webpOperation = imageManager.loadImage( + with: url, + options: [.retryFailed], + context: Util.createContext(), + progress: onProgress(_:_:_:), + completed: onLoaded(_:_:_:_:_:_:) + ) + } + } + + private func setImage(_ image: UIImage) { + if self.imageView.image == nil || image.sd_isAnimated { + self.imageView.image = image + } + + if image.sd_isAnimated { + self.firePlayerStateChange() + if isPlaying { + self.imageView.startAnimating() + } + } + } + + // MARK: - Loading blocks + + private func onProgress(_ receivedSize: Int, _ expectedSize: Int, _ imageUrl: URL?) {} + + private func onLoaded( + _ image: UIImage?, + _ data: Data?, + _ error: Error?, + _ cacheType: SDImageCacheType, + _ finished: Bool, + _ imageUrl: URL? + ) { + guard finished else { + return + } + + if let placeholderSource = self.placeholderSource, + imageUrl?.absoluteString == placeholderSource, + self.imageView.image == nil, + let image = image + { + self.setImage(image) + return + } + + if let source = self.source, + imageUrl?.absoluteString == source, + // UIImage perf suckssss if the image is animated + let data = data, + let animatedImage = SDAnimatedImage(data: data) + { + self.placeholderOperation?.cancel() + self.isPlaying = self.autoplay + self.isLoaded = true + self.setImage(animatedImage) + self.firePlayerStateChange() + } + } + + // MARK: - Playback Controls + + func play() { + self.imageView.startAnimating() + self.isPlaying = true + self.firePlayerStateChange() + } + + func pause() { + self.imageView.stopAnimating() + self.isPlaying = false + self.firePlayerStateChange() + } + + func toggle() { + if self.isPlaying { + self.pause() + } else { + self.play() + } + } + + // MARK: - Util + + private func firePlayerStateChange() { + onPlayerStateChange([ + "isPlaying": self.isPlaying, + "isLoaded": self.isLoaded + ]) + } +} diff --git a/modules/expo-bluesky-gif-view/ios/Util.swift b/modules/expo-bluesky-gif-view/ios/Util.swift new file mode 100644 index 0000000000..55ed4152aa --- /dev/null +++ b/modules/expo-bluesky-gif-view/ios/Util.swift @@ -0,0 +1,17 @@ +import SDWebImage + +class Util { + static func createContext() -> SDWebImageContext { + var context = SDWebImageContext() + + // SDAnimatedImage for some reason has issues whenever loaded from memory. Instead, we + // will just use the disk. SDWebImage will manage this cache for us, so we don't need + // to worry about clearing it. + context[.originalQueryCacheType] = SDImageCacheType.disk.rawValue + context[.originalStoreCacheType] = SDImageCacheType.disk.rawValue + context[.queryCacheType] = SDImageCacheType.disk.rawValue + context[.storeCacheType] = SDImageCacheType.disk.rawValue + + return context + } +} diff --git a/modules/expo-bluesky-gif-view/src/GifView.tsx b/modules/expo-bluesky-gif-view/src/GifView.tsx new file mode 100644 index 0000000000..87258de17b --- /dev/null +++ b/modules/expo-bluesky-gif-view/src/GifView.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import {requireNativeModule} from 'expo' +import {requireNativeViewManager} from 'expo-modules-core' + +import {GifViewProps} from './GifView.types' + +const NativeModule = requireNativeModule('ExpoBlueskyGifView') +const NativeView: React.ComponentType< + GifViewProps & {ref: React.RefObject} +> = requireNativeViewManager('ExpoBlueskyGifView') + +export class GifView extends React.PureComponent { + // TODO native types, should all be the same as those in this class + private nativeRef: React.RefObject = React.createRef() + + constructor(props: GifViewProps | Readonly) { + super(props) + } + + static async prefetchAsync(sources: string[]): Promise { + return await NativeModule.prefetchAsync(sources) + } + + async playAsync(): Promise { + await this.nativeRef.current.playAsync() + } + + async pauseAsync(): Promise { + await this.nativeRef.current.pauseAsync() + } + + async toggleAsync(): Promise { + await this.nativeRef.current.toggleAsync() + } + + render() { + return + } +} diff --git a/modules/expo-bluesky-gif-view/src/GifView.types.ts b/modules/expo-bluesky-gif-view/src/GifView.types.ts new file mode 100644 index 0000000000..29ec277f2b --- /dev/null +++ b/modules/expo-bluesky-gif-view/src/GifView.types.ts @@ -0,0 +1,15 @@ +import {ViewProps} from 'react-native' + +export interface GifViewStateChangeEvent { + nativeEvent: { + isPlaying: boolean + isLoaded: boolean + } +} + +export interface GifViewProps extends ViewProps { + autoplay?: boolean + source?: string + placeholderSource?: string + onPlayerStateChange?: (event: GifViewStateChangeEvent) => void +} diff --git a/modules/expo-bluesky-gif-view/src/GifView.web.tsx b/modules/expo-bluesky-gif-view/src/GifView.web.tsx new file mode 100644 index 0000000000..c197e01a1d --- /dev/null +++ b/modules/expo-bluesky-gif-view/src/GifView.web.tsx @@ -0,0 +1,82 @@ +import * as React from 'react' +import {StyleSheet} from 'react-native' + +import {GifViewProps} from './GifView.types' + +export class GifView extends React.PureComponent { + private readonly videoPlayerRef: React.RefObject = + React.createRef() + private isLoaded = false + + constructor(props: GifViewProps | Readonly) { + super(props) + } + + componentDidUpdate(prevProps: Readonly) { + if (prevProps.autoplay !== this.props.autoplay) { + if (this.props.autoplay) { + this.playAsync() + } else { + this.pauseAsync() + } + } + } + + static async prefetchAsync(_: string[]): Promise { + console.warn('prefetchAsync is not supported on web') + } + + private firePlayerStateChangeEvent = () => { + this.props.onPlayerStateChange?.({ + nativeEvent: { + isPlaying: !this.videoPlayerRef.current?.paused, + isLoaded: this.isLoaded, + }, + }) + } + + private onLoad = () => { + // Prevent multiple calls to onLoad because onCanPlay will fire after each loop + if (this.isLoaded) { + return + } + + this.isLoaded = true + this.firePlayerStateChangeEvent() + } + + async playAsync(): Promise { + this.videoPlayerRef.current?.play() + } + + async pauseAsync(): Promise { + this.videoPlayerRef.current?.pause() + } + + async toggleAsync(): Promise { + if (this.videoPlayerRef.current?.paused) { + await this.playAsync() + } else { + await this.pauseAsync() + } + } + + render() { + return ( +

+ {props.children} + {isVisible && ( + +
+
+ +
+
+
+ )} +
+ ) +} + +let Card = ({did, hide}: {did: string; hide: () => void}): React.ReactNode => { + const t = useTheme() + + const profile = useProfileQuery({did}) + const moderationOpts = useModerationOpts() + + const data = profile.data + + return ( + + {data && moderationOpts ? ( + + ) : ( + + + + )} + + ) +} +Card = React.memo(Card) + +function Inner({ + profile, + moderationOpts, + hide, +}: { + profile: AppBskyActorDefs.ProfileViewDetailed + moderationOpts: ModerationOpts + hide: () => void +}) { + const t = useTheme() + const {_} = useLingui() + const {currentAccount} = useSession() + const moderation = React.useMemo( + () => moderateProfile(profile, moderationOpts), + [profile, moderationOpts], + ) + const [descriptionRT] = useRichText(profile.description ?? '') + const profileShadow = useProfileShadow(profile) + const {follow, unfollow} = useFollowMethods({ + profile: profileShadow, + logContext: 'ProfileHoverCard', + }) + const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy + const following = formatCount(profile.followsCount || 0) + const followers = formatCount(profile.followersCount || 0) + const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower') + const profileURL = makeProfileLink({ + did: profile.did, + handle: profile.handle, + }) + const isMe = React.useMemo( + () => currentAccount?.did === profile.did, + [currentAccount, profile], + ) + + return ( + + + + + + + {!isMe && ( + + )} + + + + + + {sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + moderation.ui('displayName'), + )} + + + + + + + {!blockHide && ( + <> + + + + {followers} + + {pluralizedFollowers} + + + + + + {following} + following + + + + + {profile.description?.trim() && !moderation.ui('profileView').blur ? ( + + + + ) : undefined} + + )} + + ) +} diff --git a/src/components/ProfileHoverCard/types.ts b/src/components/ProfileHoverCard/types.ts new file mode 100644 index 0000000000..a62279c96c --- /dev/null +++ b/src/components/ProfileHoverCard/types.ts @@ -0,0 +1,7 @@ +import React from 'react' + +export type ProfileHoverCardProps = { + children: React.ReactElement + did: string + inline?: boolean +} diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx index 892e55489c..40d655aa90 100644 --- a/src/components/ReportDialog/SubmitView.tsx +++ b/src/components/ReportDialog/SubmitView.tsx @@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react' import {getLabelingServiceTitle} from '#/lib/moderation' import {ReportOption} from '#/lib/moderation/useReportOptions' -import {getAgent} from '#/state/session' +import {useAgent} from '#/state/session' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import * as Toast from '#/view/com/util/Toast' import {atoms as a, native, useTheme} from '#/alf' @@ -35,6 +35,7 @@ export function SubmitView({ }) { const t = useTheme() const {_} = useLingui() + const {getAgent} = useAgent() const [details, setDetails] = React.useState('') const [submitting, setSubmitting] = React.useState(false) const [selectedServices, setSelectedServices] = React.useState([ @@ -90,6 +91,7 @@ export function SubmitView({ selectedServices, onSubmitComplete, setError, + getAgent, ]) return ( diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 5cfa0b24f9..0d49e7130d 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -2,12 +2,15 @@ import React from 'react' import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' +import {NavigationProp} from '#/lib/routes/types' import {toShortUrl} from '#/lib/strings/url-helpers' import {isNative} from '#/platform/detection' import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf' import {useInteractionState} from '#/components/hooks/useInteractionState' -import {InlineLinkText} from '#/components/Link' +import {InlineLinkText, LinkProps} from '#/components/Link' +import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {TagMenu, useTagMenuControl} from '#/components/TagMenu' import {Text, TextProps} from '#/components/Typography' @@ -22,6 +25,7 @@ export function RichText({ selectable, enableTags = false, authorHandle, + onLinkPress, }: TextStyleProp & Pick & { value: RichTextAPI | string @@ -30,6 +34,7 @@ export function RichText({ disableLinks?: boolean enableTags?: boolean authorHandle?: string + onLinkPress?: LinkProps['onPress'] }) { const richText = React.useMemo( () => @@ -84,15 +89,17 @@ export function RichText({ !disableLinks ) { els.push( - - {segment.text} - , + + + {segment.text} + + , ) } else if (link && AppBskyRichtextFacet.validateLink(link).success) { if (disableLinks) { @@ -106,7 +113,8 @@ export function RichText({ style={[...styles, {pointerEvents: 'auto'}]} // @ts-ignore TODO dataSet={WORD_WRAP} - shareOnLongPress> + shareOnLongPress + onPress={onLinkPress}> {toShortUrl(segment.text)} , ) @@ -172,8 +180,15 @@ function RichTextTag({ onIn: onPressIn, onOut: onPressOut, } = useInteractionState() + const navigation = useNavigation() + + const navigateToPage = React.useCallback(() => { + navigation.push('Hashtag', { + tag: encodeURIComponent(tag), + }) + }, [navigation, tag]) - const open = React.useCallback(() => { + const openDialog = React.useCallback(() => { control.open() }, [control]) @@ -189,9 +204,10 @@ function RichTextTag({ selectable={selectable} {...native({ accessibilityLabel: _(msg`Hashtag: #${tag}`), - accessibilityHint: _(msg`Click here to open tag menu for #${tag}`), + accessibilityHint: _(msg`Long press to open tag menu for #${tag}`), accessibilityRole: isNative ? 'button' : undefined, - onPress: open, + onPress: navigateToPage, + onLongPress: openDialog, onPressIn: onPressIn, onPressOut: onPressOut, })} diff --git a/src/components/dialogs/Context.tsx b/src/components/dialogs/Context.tsx index 87bd5c2ed7..c9dff9a999 100644 --- a/src/components/dialogs/Context.tsx +++ b/src/components/dialogs/Context.tsx @@ -6,10 +6,12 @@ type Control = Dialog.DialogOuterProps['control'] type ControlsContext = { mutedWordsDialogControl: Control + signinDialogControl: Control } const ControlsContext = React.createContext({ mutedWordsDialogControl: {} as Control, + signinDialogControl: {} as Control, }) export function useGlobalDialogsControlContext() { @@ -18,9 +20,10 @@ export function useGlobalDialogsControlContext() { export function Provider({children}: React.PropsWithChildren<{}>) { const mutedWordsDialogControl = Dialog.useDialogControl() + const signinDialogControl = Dialog.useDialogControl() const ctx = React.useMemo( - () => ({mutedWordsDialogControl}), - [mutedWordsDialogControl], + () => ({mutedWordsDialogControl, signinDialogControl}), + [mutedWordsDialogControl, signinDialogControl], ) return ( diff --git a/src/components/dialogs/Embed.tsx b/src/components/dialogs/Embed.tsx new file mode 100644 index 0000000000..7d858cae40 --- /dev/null +++ b/src/components/dialogs/Embed.tsx @@ -0,0 +1,195 @@ +import React, {memo, useRef, useState} from 'react' +import {TextInput, View} from 'react-native' +import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {EMBED_SCRIPT} from '#/lib/constants' +import {niceDate} from '#/lib/strings/time' +import {toShareUrl} from '#/lib/strings/url-helpers' +import {atoms as a, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets' +import {Text} from '#/components/Typography' +import {Button, ButtonIcon, ButtonText} from '../Button' + +type EmbedDialogProps = { + control: Dialog.DialogControlProps + postAuthor: AppBskyActorDefs.ProfileViewBasic + postCid: string + postUri: string + record: AppBskyFeedPost.Record + timestamp: string +} + +let EmbedDialog = ({control, ...rest}: EmbedDialogProps): React.ReactNode => { + return ( + + + + + ) +} +EmbedDialog = memo(EmbedDialog) +export {EmbedDialog} + +function EmbedDialogInner({ + postAuthor, + postCid, + postUri, + record, + timestamp, +}: Omit) { + const t = useTheme() + const {_} = useLingui() + const ref = useRef(null) + const [copied, setCopied] = useState(false) + + // reset copied state after 2 seconds + React.useEffect(() => { + if (copied) { + const timeout = setTimeout(() => { + setCopied(false) + }, 2000) + return () => clearTimeout(timeout) + } + }, [copied]) + + const snippet = React.useMemo(() => { + function toEmbedUrl(href: string) { + return toShareUrl(href) + '?ref_src=embed' + } + + const lang = record.langs && record.langs.length > 0 ? record.langs[0] : '' + const profileHref = toEmbedUrl(['/profile', postAuthor.did].join('/')) + const urip = new AtUri(postUri) + const href = toEmbedUrl( + ['/profile', postAuthor.did, 'post', urip.rkey].join('/'), + ) + + // x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x + // DO NOT ADD ANY NEW INTERPOLATIONS BELOW WITHOUT ESCAPING THEM! + // Also, keep this code synced with the bskyembed code in landing.tsx. + // x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x + return `

${escapeHtml(record.text)}${ + record.embed + ? `

[image or embed]` + : '' + }

— ${escapeHtml( + postAuthor.displayName || postAuthor.handle, + )} (@${escapeHtml( + postAuthor.handle, + )}) ${escapeHtml( + niceDate(timestamp), + )}
` + }, [postUri, postCid, record, timestamp, postAuthor]) + + return ( + + + + Embed post + + + + Embed this post in your website. Simply copy the following snippet + and paste it into the HTML code of your website. + + + + + + + + + + + + + + ) +} + +/** + * Based on a snippet of code from React, which itself was based on the escape-html library. + * Copyright (c) Meta Platforms, Inc. and affiliates + * Copyright (c) 2012-2013 TJ Holowaychuk + * Copyright (c) 2015 Andreas Lubbe + * Copyright (c) 2015 Tiancheng "Timothy" Gu + * Licensed as MIT. + */ +const matchHtmlRegExp = /["'&<>]/ +function escapeHtml(string: string) { + const str = String(string) + const match = matchHtmlRegExp.exec(str) + if (!match) { + return str + } + let escape + let html = '' + let index + let lastIndex = 0 + for (index = match.index; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: // " + escape = '"' + break + case 38: // & + escape = '&' + break + case 39: // ' + escape = ''' + break + case 60: // < + escape = '<' + break + case 62: // > + escape = '>' + break + default: + continue + } + if (lastIndex !== index) { + html += str.slice(lastIndex, index) + } + lastIndex = index + 1 + html += escape + } + return lastIndex !== index ? html + str.slice(lastIndex, index) : html +} diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx new file mode 100644 index 0000000000..024188ec44 --- /dev/null +++ b/src/components/dialogs/GifSelect.tsx @@ -0,0 +1,305 @@ +import React, {useCallback, useMemo, useRef, useState} from 'react' +import {Keyboard, TextInput, View} from 'react-native' +import {Image} from 'expo-image' +import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logEvent} from '#/lib/statsig/statsig' +import {cleanError} from '#/lib/strings/errors' +import {isWeb} from '#/platform/detection' +import { + Gif, + useFeaturedGifsQuery, + useGifSearchQuery, +} from '#/state/queries/tenor' +import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' +import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {useThrottledValue} from '#/components/hooks/useThrottledValue' +import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' +import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' +import {Button, ButtonIcon, ButtonText} from '../Button' +import {ListFooter, ListMaybePlaceholder} from '../Lists' + +export function GifSelectDialog({ + control, + onClose, + onSelectGif: onSelectGifProp, +}: { + control: Dialog.DialogControlProps + onClose: () => void + onSelectGif: (gif: Gif) => void +}) { + const onSelectGif = useCallback( + (gif: Gif) => { + control.close(() => onSelectGifProp(gif)) + }, + [control, onSelectGifProp], + ) + + const renderErrorBoundary = useCallback( + (error: any) => , + [], + ) + + return ( + + + + + + + ) +} + +function GifList({ + control, + onSelectGif, +}: { + control: Dialog.DialogControlProps + onSelectGif: (gif: Gif) => void +}) { + const {_} = useLingui() + const t = useTheme() + const {gtMobile} = useBreakpoints() + const textInputRef = useRef(null) + const listRef = useRef(null) + const [undeferredSearch, setSearch] = useState('') + const search = useThrottledValue(undeferredSearch, 500) + + const isSearching = search.length > 0 + + const trendingQuery = useFeaturedGifsQuery() + const searchQuery = useGifSearchQuery(search) + + const { + data, + fetchNextPage, + isFetchingNextPage, + hasNextPage, + error, + isLoading, + isError, + refetch, + } = isSearching ? searchQuery : trendingQuery + + const flattenedData = useMemo(() => { + return data?.pages.flatMap(page => page.results) || [] + }, [data]) + + const renderItem = useCallback( + ({item}: {item: Gif}) => { + return + }, + [onSelectGif], + ) + + const onEndReached = React.useCallback(() => { + if (isFetchingNextPage || !hasNextPage || error) return + fetchNextPage() + }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) + + const hasData = flattenedData.length > 0 + + const onGoBack = useCallback(() => { + if (isSearching) { + // clear the input and reset the state + textInputRef.current?.clear() + setSearch('') + } else { + control.close() + } + }, [control, isSearching]) + + const listHeader = useMemo(() => { + return ( + + {/* cover top corners */} + + + {!gtMobile && isWeb && ( + + )} + + + + { + setSearch(text) + listRef.current?.scrollToOffset({offset: 0, animated: false}) + }} + returnKeyType="search" + clearButtonMode="while-editing" + inputRef={textInputRef} + maxLength={50} + onKeyPress={({nativeEvent}) => { + if (nativeEvent.key === 'Escape') { + control.close() + } + }} + /> + + + ) + }, [gtMobile, t.atoms.bg, _, control]) + + return ( + <> + {gtMobile && } + + {listHeader} + {!hasData && ( + + )} + + } + stickyHeaderIndices={[0]} + onEndReached={onEndReached} + onEndReachedThreshold={4} + keyExtractor={(item: Gif) => item.id} + // @ts-expect-error web only + style={isWeb && {minHeight: '100vh'}} + onScrollBeginDrag={() => Keyboard.dismiss()} + ListFooterComponent={ + hasData ? ( + + ) : null + } + /> + + ) +} + +function GifPreview({ + gif, + onSelectGif, +}: { + gif: Gif + onSelectGif: (gif: Gif) => void +}) { + const {gtTablet} = useBreakpoints() + const {_} = useLingui() + const t = useTheme() + + const onPress = useCallback(() => { + logEvent('composer:gif:select', {}) + onSelectGif(gif) + }, [onSelectGif, gif]) + + return ( + + ) +} + +function DialogError({details}: {details?: string}) { + const {_} = useLingui() + const control = Dialog.useDialogContext() + + return ( + + + + + + ) +} diff --git a/src/components/dialogs/Signin.tsx b/src/components/dialogs/Signin.tsx new file mode 100644 index 0000000000..b9c939e94b --- /dev/null +++ b/src/components/dialogs/Signin.tsx @@ -0,0 +1,110 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isNative} from '#/platform/detection' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useCloseAllActiveElements} from '#/state/util' +import {Logo} from '#/view/icons/Logo' +import {Logotype} from '#/view/icons/Logotype' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {Text} from '#/components/Typography' + +export function SigninDialog() { + const {signinDialogControl: control} = useGlobalDialogsControlContext() + return ( + + + + + ) +} + +function SigninDialogInner({}: {control: Dialog.DialogOuterProps['control']}) { + const t = useTheme() + const {_} = useLingui() + const {gtMobile} = useBreakpoints() + const {requestSwitchToAccount} = useLoggedOutViewControls() + const closeAllActiveElements = useCloseAllActiveElements() + + const showSignIn = React.useCallback(() => { + closeAllActiveElements() + requestSwitchToAccount({requestedAccount: 'none'}) + }, [requestSwitchToAccount, closeAllActiveElements]) + + const showCreateAccount = React.useCallback(() => { + closeAllActiveElements() + requestSwitchToAccount({requestedAccount: 'new'}) + }, [requestSwitchToAccount, closeAllActiveElements]) + + return ( + + + + + + + + + + + + Sign in or create your account to join the conversation! + + + + + + + + + + {isNative && } + + + + + ) +} diff --git a/src/components/dialogs/SwitchAccount.tsx b/src/components/dialogs/SwitchAccount.tsx index 645113d4af..55628a790d 100644 --- a/src/components/dialogs/SwitchAccount.tsx +++ b/src/components/dialogs/SwitchAccount.tsx @@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {type SessionAccount, useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' -import {useCloseAllActiveElements} from '#/state/util' import {atoms as a} from '#/alf' import * as Dialog from '#/components/Dialog' import {AccountList} from '../AccountList' @@ -21,23 +20,25 @@ export function SwitchAccountDialog({ const {currentAccount} = useSession() const {onPressSwitchAccount} = useAccountSwitcher() const {setShowLoggedOut} = useLoggedOutViewControls() - const closeAllActiveElements = useCloseAllActiveElements() const onSelectAccount = useCallback( (account: SessionAccount) => { - if (account.did === currentAccount?.did) { - control.close() + if (account.did !== currentAccount?.did) { + control.close(() => { + onPressSwitchAccount(account, 'SwitchAccount') + }) } else { - onPressSwitchAccount(account, 'SwitchAccount') + control.close() } }, [currentAccount, control, onPressSwitchAccount], ) const onPressAddAccount = useCallback(() => { - setShowLoggedOut(true) - closeAllActiveElements() - }, [setShowLoggedOut, closeAllActiveElements]) + control.close(() => { + setShowLoggedOut(true) + }) + }, [setShowLoggedOut, control]) return ( diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx index 4f3767ece1..6cbabe2911 100644 --- a/src/components/forms/HostingProvider.tsx +++ b/src/components/forms/HostingProvider.tsx @@ -41,7 +41,7 @@ export function HostingProvider({ onSelect={onSelectServiceUrl} /> - - {!serviceDescription || isProcessing ? ( - - ) : ( - - )} - {!serviceDescription || isProcessing ? ( - - Processing... - - ) : undefined} - - - - - - ) -} diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 711619e85c..6f20354be0 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -1,30 +1,16 @@ -import React, {useRef, useState} from 'react' -import { - ActivityIndicator, - Keyboard, - LayoutAnimation, - TextInput, - View, -} from 'react-native' +import React from 'react' +import {Keyboard, View} from 'react-native' import {ComAtprotoServerDescribeServer} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useAnalytics} from '#/lib/analytics/analytics' -import {isNetworkError} from '#/lib/strings/errors' -import {cleanError} from '#/lib/strings/errors' -import {createFullHandle} from '#/lib/strings/handles' -import {logger} from '#/logger' -import {useSessionApi} from '#/state/session' -import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {useLogin} from '#/screens/Login/hooks/useLogin' +import {atoms as a} from '#/alf' +import {Button, ButtonText} from '#/components/Button' import {FormError} from '#/components/forms/FormError' import {HostingProvider} from '#/components/forms/HostingProvider' import * as TextField from '#/components/forms/TextField' -import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' -import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' import {FormContainer} from './FormContainer' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema @@ -33,100 +19,26 @@ export const LoginForm = ({ error, serviceUrl, serviceDescription, - initialHandle, - setError, setServiceUrl, - onPressRetryConnect, onPressBack, - onPressForgotPassword, }: { error: string serviceUrl: string serviceDescription: ServiceDescription | undefined - initialHandle: string setError: (v: string) => void setServiceUrl: (v: string) => void onPressRetryConnect: () => void onPressBack: () => void - onPressForgotPassword: () => void }) => { const {track} = useAnalytics() - const t = useTheme() - const [isProcessing, setIsProcessing] = useState(false) - const [identifier, setIdentifier] = useState(initialHandle) - const [password, setPassword] = useState('') - const passwordInputRef = useRef(null) const {_} = useLingui() - const {login} = useSessionApi() + const {openAuthSession} = useLogin() const onPressSelectService = React.useCallback(() => { Keyboard.dismiss() track('Signin:PressedSelectService') }, [track]) - const onPressNext = async () => { - if (isProcessing) return - Keyboard.dismiss() - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setError('') - setIsProcessing(true) - - try { - // try to guess the handle if the user just gave their own username - let fullIdent = identifier - if ( - !identifier.includes('@') && // not an email - !identifier.includes('.') && // not a domain - serviceDescription && - serviceDescription.availableUserDomains.length > 0 - ) { - let matched = false - for (const domain of serviceDescription.availableUserDomains) { - if (fullIdent.endsWith(domain)) { - matched = true - } - } - if (!matched) { - fullIdent = createFullHandle( - identifier, - serviceDescription.availableUserDomains[0], - ) - } - } - - // TODO remove double login - await login( - { - service: serviceUrl, - identifier: fullIdent, - password, - }, - 'LoginForm', - ) - } catch (e: any) { - const errMsg = e.toString() - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setIsProcessing(false) - if (errMsg.includes('Authentication Required')) { - logger.debug('Failed to login due to invalid credentials', { - error: errMsg, - }) - setError(_(msg`Invalid username or password`)) - } else if (isNetworkError(e)) { - logger.warn('Failed to login due to network error', {error: errMsg}) - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - } else { - logger.warn('Failed to login', {error: errMsg}) - setError(cleanError(errMsg)) - } - } - } - - const isReady = !!serviceDescription && !!identifier && !!password return ( Sign in}> @@ -139,84 +51,8 @@ export const LoginForm = ({ onOpenDialog={onPressSelectService} /> - - - Account - - - - - { - passwordInputRef.current?.focus() - }} - blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field - value={identifier} - onChangeText={str => - setIdentifier((str || '').toLowerCase().trim()) - } - editable={!isProcessing} - accessibilityHint={_( - msg`Input the username or email address you used at signup`, - )} - /> - - - - - - - - - - + - - {!serviceDescription && error ? ( - - ) : !serviceDescription ? ( - <> - - - Connecting... - - - ) : isReady ? ( - - ) : undefined} + ) diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx deleted file mode 100644 index 5407f3f1e3..0000000000 --- a/src/screens/Login/PasswordUpdatedForm.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, {useEffect} from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useAnalytics} from '#/lib/analytics/analytics' -import {atoms as a, useBreakpoints} from '#/alf' -import {Button, ButtonText} from '#/components/Button' -import {Text} from '#/components/Typography' -import {FormContainer} from './FormContainer' - -export const PasswordUpdatedForm = ({ - onPressNext, -}: { - onPressNext: () => void -}) => { - const {screen} = useAnalytics() - const {_} = useLingui() - const {gtMobile} = useBreakpoints() - - useEffect(() => { - screen('Signin:PasswordUpdatedForm') - }, [screen]) - - return ( - - - Password updated! - - - You can now sign in with your new password. - - - - - - ) -} diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx index 88f7ec5416..e69de29bb2 100644 --- a/src/screens/Login/SetNewPasswordForm.tsx +++ b/src/screens/Login/SetNewPasswordForm.tsx @@ -1,192 +0,0 @@ -import React, {useEffect, useState} from 'react' -import {ActivityIndicator, View} from 'react-native' -import {BskyAgent} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useAnalytics} from '#/lib/analytics/analytics' -import {isNetworkError} from '#/lib/strings/errors' -import {cleanError} from '#/lib/strings/errors' -import {checkAndFormatResetCode} from '#/lib/strings/password' -import {logger} from '#/logger' -import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonText} from '#/components/Button' -import {FormError} from '#/components/forms/FormError' -import * as TextField from '#/components/forms/TextField' -import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' -import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' -import {Text} from '#/components/Typography' -import {FormContainer} from './FormContainer' - -export const SetNewPasswordForm = ({ - error, - serviceUrl, - setError, - onPressBack, - onPasswordSet, -}: { - error: string - serviceUrl: string - setError: (v: string) => void - onPressBack: () => void - onPasswordSet: () => void -}) => { - const {screen} = useAnalytics() - const {_} = useLingui() - const t = useTheme() - - useEffect(() => { - screen('Signin:SetNewPasswordForm') - }, [screen]) - - const [isProcessing, setIsProcessing] = useState(false) - const [resetCode, setResetCode] = useState('') - const [password, setPassword] = useState('') - - const onPressNext = async () => { - // Check that the code is correct. We do this again just incase the user enters the code after their pw and we - // don't get to call onBlur first - const formattedCode = checkAndFormatResetCode(resetCode) - // TODO Better password strength check - if (!formattedCode || !password) { - setError( - _( - msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, - ), - ) - return - } - - setError('') - setIsProcessing(true) - - try { - const agent = new BskyAgent({service: serviceUrl}) - await agent.com.atproto.server.resetPassword({ - token: formattedCode, - password, - }) - onPasswordSet() - } catch (e: any) { - const errMsg = e.toString() - logger.warn('Failed to set new password', {error: e}) - setIsProcessing(false) - if (isNetworkError(e)) { - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - } else { - setError(cleanError(errMsg)) - } - } - } - - const onBlur = () => { - const formattedCode = checkAndFormatResetCode(resetCode) - if (!formattedCode) { - setError( - _( - msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, - ), - ) - return - } - setResetCode(formattedCode) - } - - return ( - Set new password}> - - - You will receive an email with a "reset code." Enter that code here, - then enter your new password. - - - - - Reset code - - - setError('')} - onBlur={onBlur} - editable={!isProcessing} - accessibilityHint={_( - msg`Input code sent to your email for password reset`, - )} - /> - - - - - New password - - - - - - - - - - - - {isProcessing ? ( - - ) : ( - - )} - {isProcessing ? ( - - Updating... - - ) : undefined} - - - ) -} diff --git a/src/screens/Login/hooks/package.json b/src/screens/Login/hooks/package.json new file mode 100644 index 0000000000..3a1fb0a9b1 --- /dev/null +++ b/src/screens/Login/hooks/package.json @@ -0,0 +1,14 @@ +{ + "name": "hooks", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/social-app.git" + }, + "private": true +} diff --git a/src/screens/Login/hooks/useLogin.ts b/src/screens/Login/hooks/useLogin.ts new file mode 100644 index 0000000000..1aa01b040d --- /dev/null +++ b/src/screens/Login/hooks/useLogin.ts @@ -0,0 +1,48 @@ +import React from 'react' +import * as Browser from 'expo-web-browser' + +import { + DPOP_BOUND_ACCESS_TOKENS, + OAUTH_APPLICATION_TYPE, + OAUTH_CLIENT_ID, + OAUTH_GRANT_TYPES, + OAUTH_REDIRECT_URI, + OAUTH_RESPONSE_TYPES, + OAUTH_SCOPE, +} from 'lib/oauth' +import {RNOAuthClientFactory} from '../../../../modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native' + +// Service URL here is just a placeholder, this isn't how it will actually work +export function useLogin() { + const openAuthSession = React.useCallback(async () => { + const oauthFactory = new RNOAuthClientFactory({ + clientMetadata: { + client_id: OAUTH_CLIENT_ID, + redirect_uris: [OAUTH_REDIRECT_URI], + grant_types: OAUTH_GRANT_TYPES, + response_types: OAUTH_RESPONSE_TYPES, + scope: OAUTH_SCOPE, + dpop_bound_access_tokens: DPOP_BOUND_ACCESS_TOKENS, + application_type: OAUTH_APPLICATION_TYPE, + }, + fetch: global.fetch, + }) + + const url = await oauthFactory.signIn('http://localhost:2583/') + + console.log(url.href) + + const authSession = await Browser.openAuthSessionAsync( + url.href, + OAUTH_REDIRECT_URI, + ) + + if (authSession.type !== 'success') { + return + } + }, []) + + return { + openAuthSession, + } +} diff --git a/src/screens/Login/hooks/useLogin.web.ts b/src/screens/Login/hooks/useLogin.web.ts new file mode 100644 index 0000000000..6c16bc1810 --- /dev/null +++ b/src/screens/Login/hooks/useLogin.web.ts @@ -0,0 +1,13 @@ +import React from 'react' + +export function useLogin(serviceUrl: string | undefined) { + const openAuthSession = React.useCallback(async () => { + if (!serviceUrl) return + + window.location.href = serviceUrl + }, [serviceUrl]) + + return { + openAuthSession, + } +} diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 1fce63d298..42b355a730 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -4,17 +4,13 @@ import {LayoutAnimationConfig} from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useAnalytics} from '#/lib/analytics/analytics' import {DEFAULT_SERVICE} from '#/lib/constants' import {logger} from '#/logger' import {useServiceQuery} from '#/state/queries/service' import {SessionAccount, useSession} from '#/state/session' import {useLoggedOutView} from '#/state/shell/logged-out' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' -import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' import {LoginForm} from '#/screens/Login/LoginForm' -import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' -import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' import {atoms as a} from '#/alf' import {ChooseAccountForm} from './ChooseAccountForm' import {ScreenTransition} from './ScreenTransition' @@ -22,16 +18,12 @@ import {ScreenTransition} from './ScreenTransition' enum Forms { Login, ChooseAccount, - ForgotPassword, - SetNewPassword, - PasswordUpdated, } export const Login = ({onPressBack}: {onPressBack: () => void}) => { const {_} = useLingui() const {accounts} = useSession() - const {track} = useAnalytics() const {requestedAccountSwitchTo} = useLoggedOutView() const requestedAccount = accounts.find( acc => acc.did === requestedAccountSwitchTo, @@ -41,9 +33,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { const [serviceUrl, setServiceUrl] = React.useState( requestedAccount?.service || DEFAULT_SERVICE, ) - const [initialHandle, setInitialHandle] = React.useState( - requestedAccount?.handle || '', - ) const [currentForm, setCurrentForm] = React.useState( requestedAccount ? Forms.Login @@ -62,7 +51,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { if (account?.service) { setServiceUrl(account.service) } - setInitialHandle(account?.handle || '') + // TODO set the service URL. We really need to fix this though in general setCurrentForm(Forms.Login) } @@ -86,11 +75,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { } }, [serviceError, serviceUrl, _]) - const onPressForgotPassword = () => { - track('Signin:PressedForgotPassword') - setCurrentForm(Forms.ForgotPassword) - } - let content = null let title = '' let description = '' @@ -104,13 +88,11 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { error={error} serviceUrl={serviceUrl} serviceDescription={serviceDescription} - initialHandle={initialHandle} setError={setError} setServiceUrl={setServiceUrl} onPressBack={() => accounts.length ? gotoForm(Forms.ChooseAccount) : onPressBack() } - onPressForgotPassword={onPressForgotPassword} onPressRetryConnect={refetchService} /> ) @@ -125,41 +107,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { /> ) break - case Forms.ForgotPassword: - title = _(msg`Forgot Password`) - description = _(msg`Let's get your password reset!`) - content = ( - gotoForm(Forms.Login)} - onEmailSent={() => gotoForm(Forms.SetNewPassword)} - /> - ) - break - case Forms.SetNewPassword: - title = _(msg`Forgot Password`) - description = _(msg`Let's get your password reset!`) - content = ( - gotoForm(Forms.ForgotPassword)} - onPasswordSet={() => gotoForm(Forms.PasswordUpdated)} - /> - ) - break - case Forms.PasswordUpdated: - title = _(msg`Password updated`) - description = _(msg`You can now sign in with your new password.`) - content = ( - gotoForm(Forms.Login)} /> - ) - break } return ( diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx new file mode 100644 index 0000000000..239425a2f5 --- /dev/null +++ b/src/screens/Messages/Conversation/index.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {CommonNavigatorParams} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' +import {ViewHeader} from '#/view/com/util/ViewHeader' +import {ClipClopGate} from '../gate' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'MessagesConversation' +> +export function MessagesConversationScreen({route}: Props) { + const chatId = route.params.conversation + const {_} = useLingui() + + const gate = useGate() + if (!gate('dms')) return + + return ( + + + + ) +} diff --git a/src/screens/Messages/List/index.tsx b/src/screens/Messages/List/index.tsx new file mode 100644 index 0000000000..c4490aa5c5 --- /dev/null +++ b/src/screens/Messages/List/index.tsx @@ -0,0 +1,229 @@ +import React, {useCallback, useState} from 'react' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {useInfiniteQuery} from '@tanstack/react-query' + +import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {MessagesTabNavigatorParams} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' +import {cleanError} from '#/lib/strings/errors' +import {logger} from '#/logger' +import {useAgent} from '#/state/session' +import {List} from '#/view/com/util/List' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' +import {ViewHeader} from '#/view/com/util/ViewHeader' +import {useTheme} from '#/alf' +import {atoms as a} from '#/alf' +import {SettingsSliderVertical_Stroke2_Corner0_Rounded as SettingsSlider} from '#/components/icons/SettingsSlider' +import {Link} from '#/components/Link' +import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' +import {Text} from '#/components/Typography' +import {ClipClopGate} from '../gate' + +type Props = NativeStackScreenProps +export function MessagesListScreen({}: Props) { + const {_} = useLingui() + const t = useTheme() + + const renderButton = useCallback(() => { + return ( + + + + ) + }, [_, t.atoms.text]) + + const initialNumToRender = useInitialNumToRender() + const [isPTRing, setIsPTRing] = useState(false) + + const { + data, + isLoading, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + error, + refetch, + } = usePlaceholderConversations() + + const isError = !!error + + const conversations = React.useMemo(() => { + if (data?.pages) { + return data.pages.flat() + } + return [] + }, [data]) + + const onRefresh = React.useCallback(async () => { + setIsPTRing(true) + try { + await refetch() + } catch (err) { + logger.error('Failed to refresh conversations', {message: err}) + } + setIsPTRing(false) + }, [refetch, setIsPTRing]) + + const onEndReached = React.useCallback(async () => { + if (isFetchingNextPage || !hasNextPage || isError) return + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more conversations', {message: err}) + } + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) + + const gate = useGate() + if (!gate('dms')) return + + if (conversations.length < 1) { + return ( + + ) + } + + return ( + + + { + return ( + + + + + + + {item.profile.displayName || item.profile.handle} + {' '} + + @{item.profile.handle} + + + {item.unread && ( + + )} + + + {item.lastMessage} + + + + ) + }} + keyExtractor={item => item.profile.did} + refreshing={isPTRing} + onRefresh={onRefresh} + onEndReached={onEndReached} + ListFooterComponent={ + + } + onEndReachedThreshold={3} + initialNumToRender={initialNumToRender} + windowSize={11} + /> + + ) +} + +function usePlaceholderConversations() { + const {getAgent} = useAgent() + + return useInfiniteQuery({ + queryKey: ['messages'], + queryFn: async () => { + const people = await getAgent().getProfiles({actors: PLACEHOLDER_PEOPLE}) + return people.data.profiles.map(profile => ({ + profile, + unread: Math.random() > 0.5, + lastMessage: getRandomPost(), + })) + }, + initialPageParam: undefined, + getNextPageParam: () => undefined, + }) +} + +const PLACEHOLDER_PEOPLE = [ + 'pfrazee.com', + 'haileyok.com', + 'danabra.mov', + 'esb.lol', + 'samuel.bsky.team', +] + +function getRandomPost() { + const num = Math.floor(Math.random() * 10) + switch (num) { + case 0: + return 'hello' + case 1: + return 'lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua' + case 2: + return 'banger post' + case 3: + return 'lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua' + case 4: + return 'lol look at this bug' + case 5: + return 'wow' + case 6: + return "that's pretty cool, wow!" + case 7: + return 'I think this is a bug' + case 8: + return 'Hello World!' + case 9: + return 'DMs when???' + default: + return 'this is unlikely' + } +} diff --git a/src/screens/Messages/Settings/index.tsx b/src/screens/Messages/Settings/index.tsx new file mode 100644 index 0000000000..bd093c7927 --- /dev/null +++ b/src/screens/Messages/Settings/index.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {CommonNavigatorParams} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' +import {ViewHeader} from '#/view/com/util/ViewHeader' +import {ClipClopGate} from '../gate' + +type Props = NativeStackScreenProps +export function MessagesSettingsScreen({}: Props) { + const {_} = useLingui() + + const gate = useGate() + if (!gate('dms')) return + + return ( + + + + ) +} diff --git a/src/screens/Messages/gate.tsx b/src/screens/Messages/gate.tsx new file mode 100644 index 0000000000..f225a0c9d1 --- /dev/null +++ b/src/screens/Messages/gate.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import {Text, View} from 'react-native' + +export function ClipClopGate() { + return ( + + 🐴 + Nice try + + ) +} diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx index 56635c672d..e7054fb1ff 100644 --- a/src/screens/Onboarding/StepFinished.tsx +++ b/src/screens/Onboarding/StepFinished.tsx @@ -8,7 +8,7 @@ import {BSKY_APP_ACCOUNT_DID} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useSetSaveFeedsMutation} from '#/state/queries/preferences' -import {getAgent} from '#/state/session' +import {useAgent} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import { DescriptionText, @@ -38,6 +38,7 @@ export function StepFinished() { const onboardDispatch = useOnboardingDispatch() const [saving, setSaving] = React.useState(false) const {mutateAsync: saveFeeds} = useSetSaveFeedsMutation() + const {getAgent} = useAgent() const finishOnboarding = React.useCallback(async () => { setSaving(true) @@ -57,6 +58,7 @@ export function StepFinished() { try { await Promise.all([ bulkWriteFollows( + getAgent, suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID), ), // these must be serial @@ -80,7 +82,7 @@ export function StepFinished() { track('OnboardingV2:StepFinished:End') track('OnboardingV2:Complete') logEvent('onboarding:finished:nextPressed', {}) - }, [state, dispatch, onboardDispatch, setSaving, saveFeeds, track]) + }, [state, dispatch, onboardDispatch, setSaving, saveFeeds, track, getAgent]) React.useEffect(() => { track('OnboardingV2:StepFinished:Start') diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx index 6afc3c07ac..df489f5710 100644 --- a/src/screens/Onboarding/StepInterests/index.tsx +++ b/src/screens/Onboarding/StepInterests/index.tsx @@ -8,7 +8,7 @@ import {useAnalytics} from '#/lib/analytics/analytics' import {logEvent} from '#/lib/statsig/statsig' import {capitalize} from '#/lib/strings/capitalize' import {logger} from '#/logger' -import {getAgent} from '#/state/session' +import {useAgent} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import { DescriptionText, @@ -39,6 +39,7 @@ export function StepInterests() { state.interestsStepResults.selectedInterests.map(i => i), ) const onboardDispatch = useOnboardingDispatch() + const {getAgent} = useAgent() const {isLoading, isError, error, data, refetch, isFetching} = useQuery({ queryKey: ['interests'], queryFn: async () => { diff --git a/src/screens/Onboarding/util.ts b/src/screens/Onboarding/util.ts index 1a0b8d21bc..fde4316e93 100644 --- a/src/screens/Onboarding/util.ts +++ b/src/screens/Onboarding/util.ts @@ -1,7 +1,10 @@ -import {AppBskyGraphFollow, AppBskyGraphGetFollows} from '@atproto/api' +import { + AppBskyGraphFollow, + AppBskyGraphGetFollows, + BskyAgent, +} from '@atproto/api' import {until} from '#/lib/async/until' -import {getAgent} from '#/state/session' import {PRIMARY_FEEDS} from './StepAlgoFeeds' function shuffle(array: any) { @@ -63,7 +66,10 @@ export function aggregateInterestItems( return Array.from(new Set(results)).slice(0, 20) } -export async function bulkWriteFollows(dids: string[]) { +export async function bulkWriteFollows( + getAgent: () => BskyAgent, + dids: string[], +) { const session = getAgent().session if (!session) { @@ -87,10 +93,15 @@ export async function bulkWriteFollows(dids: string[]) { repo: session.did, writes: followWrites, }) - await whenFollowsIndexed(session.did, res => !!res.data.follows.length) + await whenFollowsIndexed( + getAgent, + session.did, + res => !!res.data.follows.length, + ) } async function whenFollowsIndexed( + getAgent: () => BskyAgent, actor: string, fn: (res: AppBskyGraphGetFollows.Response) => boolean, ) { @@ -107,21 +118,15 @@ async function whenFollowsIndexed( } /** - * Kinda hacky, but we want For Your or Discover to appear as the first pinned + * Kinda hacky, but we want Discover to appear as the first pinned * feed after Following */ export function sortPrimaryAlgorithmFeeds(uris: string[]) { return uris.sort((a, b) => { - if (a === PRIMARY_FEEDS[0].uri) { - return -1 - } - if (b === PRIMARY_FEEDS[0].uri) { - return 1 - } - if (a === PRIMARY_FEEDS[1].uri) { + if (a === PRIMARY_FEEDS[0]?.uri) { return -1 } - if (b === PRIMARY_FEEDS[1].uri) { + if (b === PRIMARY_FEEDS[0]?.uri) { return 1 } return a.localeCompare(b) diff --git a/src/screens/Profile/Header/Handle.tsx b/src/screens/Profile/Header/Handle.tsx index fd1cbe5333..9ab24fbbed 100644 --- a/src/screens/Profile/Header/Handle.tsx +++ b/src/screens/Profile/Header/Handle.tsx @@ -1,10 +1,10 @@ import React from 'react' import {View} from 'react-native' import {AppBskyActorDefs} from '@atproto/api' -import {isInvalidHandle} from 'lib/strings/handles' -import {Shadow} from '#/state/cache/types' import {Trans} from '@lingui/macro' +import {Shadow} from '#/state/cache/types' +import {isInvalidHandle} from 'lib/strings/handles' import {atoms as a, useTheme, web} from '#/alf' import {Text} from '#/components/Typography' @@ -26,6 +26,7 @@ export function ProfileHeaderHandle({ ) : undefined} {invalidHandle ? ⚠Invalid Handle : `@${profile.handle}`} diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 4d8dbad86c..cbac0b66c3 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -10,7 +10,6 @@ import { import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {Haptics} from '#/lib/haptics' import {isAppLabeler} from '#/lib/moderation' import {pluralize} from '#/lib/strings/helpers' import {logger} from '#/logger' @@ -19,8 +18,10 @@ import {useModalControls} from '#/state/modals' import {useLabelerSubscriptionMutation} from '#/state/queries/labeler' import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useSession} from '#/state/session' +import {useRequireAuth, useSession} from '#/state/session' import {useAnalytics} from 'lib/analytics/analytics' +import {useHaptics} from 'lib/haptics' +import {isIOS} from 'platform/detection' import {useProfileShadow} from 'state/cache/profile-shadow' import {ProfileMenu} from '#/view/com/profile/ProfileMenu' import * as Toast from '#/view/com/util/Toast' @@ -64,6 +65,8 @@ let ProfileHeaderLabeler = ({ const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() const {track} = useAnalytics() + const requireAuth = useRequireAuth() + const playHaptic = useHaptics() const cantSubscribePrompt = Prompt.usePromptControl() const isSelf = currentAccount?.did === profile.did @@ -93,7 +96,7 @@ let ProfileHeaderLabeler = ({ return } try { - Haptics.default() + playHaptic() if (likeUri) { await unlikeMod({uri: likeUri}) @@ -114,7 +117,7 @@ let ProfileHeaderLabeler = ({ ) logger.error(`Failed to toggle labeler like`, {message: e.message}) } - }, [labeler, likeUri, likeMod, unlikeMod, track, _]) + }, [labeler, playHaptic, likeUri, unlikeMod, track, likeMod, _]) const onPressEditProfile = React.useCallback(() => { track('ProfileHeader:EditProfileButtonClicked') @@ -124,27 +127,32 @@ let ProfileHeaderLabeler = ({ }) }, [track, openModal, profile]) - const onPressSubscribe = React.useCallback(async () => { - if (!canSubscribe) { - cantSubscribePrompt.open() - return - } - try { - await toggleSubscription({ - did: profile.did, - subscribe: !isSubscribed, - }) - } catch (e: any) { - // setSubscriptionError(e.message) - logger.error(`Failed to subscribe to labeler`, {message: e.message}) - } - }, [ - toggleSubscription, - isSubscribed, - profile, - canSubscribe, - cantSubscribePrompt, - ]) + const onPressSubscribe = React.useCallback( + () => + requireAuth(async () => { + if (!canSubscribe) { + cantSubscribePrompt.open() + return + } + try { + await toggleSubscription({ + did: profile.did, + subscribe: !isSubscribed, + }) + } catch (e: any) { + // setSubscriptionError(e.message) + logger.error(`Failed to subscribe to labeler`, {message: e.message}) + } + }), + [ + requireAuth, + toggleSubscription, + isSubscribed, + profile, + canSubscribe, + cantSubscribePrompt, + ], + ) const isMe = React.useMemo( () => currentAccount?.did === profile.did, @@ -157,10 +165,12 @@ let ProfileHeaderLabeler = ({ moderation={moderation} hideBackButton={hideBackButton} isPlaceholderProfile={isPlaceholderProfile}> - + + pointerEvents={isIOS ? 'auto' : 'box-none'}> {isMe ? ( ) : ( - - - ) - - return ( - <> - - - {hasFeeds ? ( - } - keyExtractor={item => item.uri} - style={{flex: 1}} - /> - ) : isLoading ? ( - - - - ) : ( - - )} - - - - - - - - Check out some recommended feeds. Tap + to add them to your list - of pinned feeds. - - - - {hasFeeds ? ( - } - keyExtractor={item => item.uri} - style={{flex: 1}} - /> - ) : isLoading ? ( - - - - ) : ( - - - - )} - - - - - - - {item.likeCount || 0} - - - - - - - ) -} diff --git a/src/view/com/auth/onboarding/RecommendedFollows.tsx b/src/view/com/auth/onboarding/RecommendedFollows.tsx deleted file mode 100644 index d275f6c90e..0000000000 --- a/src/view/com/auth/onboarding/RecommendedFollows.tsx +++ /dev/null @@ -1,270 +0,0 @@ -import React from 'react' -import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {AppBskyActorDefs, moderateProfile} from '@atproto/api' -import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints' -import {Text} from 'view/com/util/text/Text' -import {ViewHeader} from 'view/com/util/ViewHeader' -import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout' -import {Button} from 'view/com/util/forms/Button' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {usePalette} from 'lib/hooks/usePalette' -import {RecommendedFollowsItem} from './RecommendedFollowsItem' -import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' -import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows' -import {useModerationOpts} from '#/state/queries/preferences' -import {logger} from '#/logger' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -type Props = { - next: () => void -} -export function RecommendedFollows({next}: Props) { - const pal = usePalette('default') - const {_} = useLingui() - const {isTabletOrMobile} = useWebMediaQueries() - const {data: suggestedFollows} = useSuggestedFollowsQuery() - const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor() - const [additionalSuggestions, setAdditionalSuggestions] = React.useState<{ - [did: string]: AppBskyActorDefs.ProfileView[] - }>({}) - const existingDids = React.useRef([]) - const moderationOpts = useModerationOpts() - - const title = ( - <> - - - Follow some - - - Recommended - - - Users - - - - - Follow some users to get started. We can recommend you more users - based on who you find interesting. - - - - - - - ) - - const suggestions = React.useMemo(() => { - if (!suggestedFollows) return [] - - const additional = Object.entries(additionalSuggestions) - const items = suggestedFollows.pages.flatMap(page => page.actors) - - outer: while (additional.length) { - const additionalAccount = additional.shift() - - if (!additionalAccount) break - - const [followedUser, relatedAccounts] = additionalAccount - - for (let i = 0; i < items.length; i++) { - if (items[i].did === followedUser) { - items.splice(i + 1, 0, ...relatedAccounts) - continue outer - } - } - } - - existingDids.current = items.map(i => i.did) - - return items - }, [suggestedFollows, additionalSuggestions]) - - const onFollowStateChange = React.useCallback( - async ({following, did}: {following: boolean; did: string}) => { - if (following) { - try { - const {suggestions: results} = await getSuggestedFollowsByActor(did) - - if (results.length) { - const deduped = results.filter( - r => !existingDids.current.find(did => did === r.did), - ) - setAdditionalSuggestions(s => ({ - ...s, - [did]: deduped.slice(0, 3), - })) - } - } catch (e) { - logger.error('RecommendedFollows: failed to get suggestions', { - message: e, - }) - } - } - - // not handling the unfollow case - }, - [existingDids, getSuggestedFollowsByActor, setAdditionalSuggestions], - ) - - return ( - <> - - - {!suggestedFollows || !moderationOpts ? ( - - ) : ( - ( - - )} - keyExtractor={item => item.did} - style={{flex: 1}} - /> - )} - - - - - - - - - - Check out some recommended users. Follow them to see similar - users. - - - - {!suggestedFollows || !moderationOpts ? ( - - ) : ( - ( - - )} - keyExtractor={item => item.did} - style={{flex: 1}} - /> - )} - - - - ) -} - -const styles = StyleSheet.create({ - row: { - flexDirection: 'row', - columnGap: 20, - alignItems: 'center', - marginVertical: 20, - }, - rowText: { - flex: 1, - }, - spacer: { - height: 20, - }, -}) diff --git a/src/view/com/auth/onboarding/WelcomeMobile.tsx b/src/view/com/auth/onboarding/WelcomeMobile.tsx deleted file mode 100644 index b8659d56cd..0000000000 --- a/src/view/com/auth/onboarding/WelcomeMobile.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react' -import {Pressable, StyleSheet, View} from 'react-native' -import {Text} from 'view/com/util/text/Text' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Button} from 'view/com/util/forms/Button' -import {ViewHeader} from 'view/com/util/ViewHeader' -import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' - -type Props = { - next: () => void - skip: () => void -} - -export function WelcomeMobile({next, skip}: Props) { - const pal = usePalette('default') - const {_} = useLingui() - - return ( - - { - return ( - - - Skip - - - - ) - }} - /> - - - - Welcome to{' '} - Bluesky - - - - - - - - Bluesky is public. - - - - Your posts, likes, and blocks are public. Mutes are private. - - - - - - - - - Bluesky is open. - - - Never lose access to your followers and data. - - - - - - - - Bluesky is flexible. - - - - Choose the algorithms that power your experience with custom - feeds. - - - - - - - + ) : null} + @@ -591,7 +611,7 @@ const styles = StyleSheet.create({ }, bottomBar: { flexDirection: 'row', - paddingVertical: 10, + paddingVertical: 4, paddingLeft: 15, paddingRight: 20, alignItems: 'center', diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index 0c1b87d04d..7dc17fd4a7 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -1,21 +1,22 @@ import React from 'react' import {LayoutAnimation, Pressable, StyleSheet, View} from 'react-native' import {Image} from 'expo-image' -import {useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' import { AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyFeedPost, } from '@atproto/api' -import {ComposerOptsPostRef} from 'state/shell/composer' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + import {usePalette} from 'lib/hooks/usePalette' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {Text} from 'view/com/util/text/Text' +import {ComposerOptsPostRef} from 'state/shell/composer' import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed' +import {Text} from 'view/com/util/text/Text' +import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { const pal = usePalette('default') @@ -83,9 +84,9 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { accessibilityHint={_( msg`Expand or collapse the full post you are replying to`, )}> - @@ -216,6 +217,7 @@ function ComposerReplyToImages({ const styles = StyleSheet.create({ replyToLayout: { flexDirection: 'row', + alignItems: 'flex-start', borderTopWidth: 1, paddingTop: 16, paddingBottom: 16, diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx index 02dd1bbd75..321e29b30a 100644 --- a/src/view/com/composer/ExternalEmbed.tsx +++ b/src/view/com/composer/ExternalEmbed.tsx @@ -1,70 +1,82 @@ import React from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' +import {StyleProp, TouchableOpacity, View, ViewStyle} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {AutoSizedImage} from '../util/images/AutoSizedImage' -import {Text} from '../util/text/Text' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {ExternalEmbedDraft} from 'lib/api/index' -import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {ExternalEmbedDraft} from 'lib/api/index' +import {s} from 'lib/styles' +import {Gif} from 'state/queries/tenor' +import {ExternalLinkEmbed} from 'view/com/util/post-embeds/ExternalLinkEmbed' +import {atoms as a, useTheme} from '#/alf' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' export const ExternalEmbed = ({ link, onRemove, + gif, }: { link?: ExternalEmbedDraft onRemove: () => void + gif?: Gif }) => { - const pal = usePalette('default') - const palError = usePalette('error') + const t = useTheme() const {_} = useLingui() - if (!link) { - return - } + + const linkInfo = React.useMemo( + () => + link && { + title: link.meta?.title ?? link.uri, + uri: link.uri, + description: link.meta?.description ?? '', + thumb: link.localThumb?.path, + }, + [link], + ) + + if (!link) return null + + const loadingStyle: ViewStyle | undefined = gif + ? { + aspectRatio: + gif.media_formats.gif.dims[0] / gif.media_formats.gif.dims[1], + width: '100%', + } + : undefined + return ( - + {link.isLoading ? ( - - - - ) : link.localThumb ? ( - - ) : undefined} - - {!!link.meta?.title && ( - - {link.meta.title} + + + + ) : link.meta?.error ? ( + + + {link.uri} - )} - - {link.uri} - - {!!link.meta?.description && ( - - {link.meta.description} + + {link.meta?.error} - )} - {link.meta?.error ? ( - - {link.meta.error} - - ) : null} - + + ) : linkInfo ? ( + + + + ) : null} + children: React.ReactNode +}) { + const t = useTheme() + return ( + + {children} + + ) +} diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index 4353704d57..8f9152e34d 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -1,32 +1,31 @@ import React, {useCallback} from 'react' -import {TouchableOpacity, StyleSheet} from 'react-native' import * as MediaLibrary from 'expo-media-library' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {usePalette} from 'lib/hooks/usePalette' -import {useAnalytics} from 'lib/analytics/analytics' -import {openCamera} from 'lib/media/picker' -import {useCameraPermission} from 'lib/hooks/usePermissions' -import {HITSLOP_10, POST_IMG_MAX} from 'lib/constants' -import {GalleryModel} from 'state/models/media/gallery' -import {isMobileWeb, isNative} from 'platform/detection' -import {logger} from '#/logger' -import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useAnalytics} from '#/lib/analytics/analytics' +import {POST_IMG_MAX} from '#/lib/constants' +import {useCameraPermission} from '#/lib/hooks/usePermissions' +import {openCamera} from '#/lib/media/picker' +import {logger} from '#/logger' +import {isMobileWeb, isNative} from '#/platform/detection' +import {GalleryModel} from '#/state/models/media/gallery' +import {atoms as a, useTheme} from '#/alf' +import {Button} from '#/components/Button' +import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera' type Props = { gallery: GalleryModel + disabled?: boolean } -export function OpenCameraBtn({gallery}: Props) { - const pal = usePalette('default') +export function OpenCameraBtn({gallery, disabled}: Props) { const {track} = useAnalytics() const {_} = useLingui() const {requestCameraAccessIfNeeded} = useCameraPermission() const [mediaPermissionRes, requestMediaPermission] = MediaLibrary.usePermissions() + const t = useTheme() const onPressTakePicture = useCallback(async () => { track('Composer:CameraOpened') @@ -68,25 +67,17 @@ export function OpenCameraBtn({gallery}: Props) { } return ( - - - + label={_(msg`Camera`)} + accessibilityHint={_(msg`Opens camera on device`)} + style={a.p_sm} + variant="ghost" + shape="round" + color="primary" + disabled={disabled}> + + ) } - -const styles = StyleSheet.create({ - button: { - paddingHorizontal: 15, - }, -}) diff --git a/src/view/com/composer/photos/SelectGifBtn.tsx b/src/view/com/composer/photos/SelectGifBtn.tsx new file mode 100644 index 0000000000..60cef9a192 --- /dev/null +++ b/src/view/com/composer/photos/SelectGifBtn.tsx @@ -0,0 +1,53 @@ +import React, {useCallback} from 'react' +import {Keyboard} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logEvent} from '#/lib/statsig/statsig' +import {Gif} from '#/state/queries/tenor' +import {atoms as a, useTheme} from '#/alf' +import {Button} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {GifSelectDialog} from '#/components/dialogs/GifSelect' +import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif' + +type Props = { + onClose: () => void + onSelectGif: (gif: Gif) => void + disabled?: boolean +} + +export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) { + const {_} = useLingui() + const control = useDialogControl() + const t = useTheme() + + const onPressSelectGif = useCallback(async () => { + logEvent('composer:gif:open', {}) + Keyboard.dismiss() + control.open() + }, [control]) + + return ( + <> + + + + + ) +} diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx index f7fa9502d6..747653fc8d 100644 --- a/src/view/com/composer/photos/SelectPhotoBtn.tsx +++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx @@ -1,27 +1,26 @@ +/* eslint-disable react-native-a11y/has-valid-accessibility-ignores-invert-colors */ import React, {useCallback} from 'react' -import {TouchableOpacity, StyleSheet} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {usePalette} from 'lib/hooks/usePalette' -import {useAnalytics} from 'lib/analytics/analytics' -import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions' -import {GalleryModel} from 'state/models/media/gallery' -import {HITSLOP_10} from 'lib/constants' -import {isNative} from 'platform/detection' -import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useAnalytics} from '#/lib/analytics/analytics' +import {usePhotoLibraryPermission} from '#/lib/hooks/usePermissions' +import {isNative} from '#/platform/detection' +import {GalleryModel} from '#/state/models/media/gallery' +import {atoms as a, useTheme} from '#/alf' +import {Button} from '#/components/Button' +import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image' type Props = { gallery: GalleryModel + disabled?: boolean } -export function SelectPhotoBtn({gallery}: Props) { - const pal = usePalette('default') +export function SelectPhotoBtn({gallery, disabled}: Props) { const {track} = useAnalytics() const {_} = useLingui() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() + const t = useTheme() const onPressSelectPhotos = useCallback(async () => { track('Composer:GalleryOpened') @@ -34,25 +33,17 @@ export function SelectPhotoBtn({gallery}: Props) { }, [track, requestPhotoAccessIfNeeded, gallery]) return ( - - - + label={_(msg`Gallery`)} + accessibilityHint={_(msg`Opens device photo gallery`)} + style={a.p_sm} + variant="ghost" + shape="round" + color="primary" + disabled={disabled}> + + ) } - -const styles = StyleSheet.create({ - button: { - paddingHorizontal: 15, - }, -}) diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 20be585c25..cb16e3c666 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -1,10 +1,10 @@ import React, { + ComponentProps, forwardRef, useCallback, - useRef, useMemo, + useRef, useState, - ComponentProps, } from 'react' import { NativeSyntheticEvent, @@ -13,22 +13,26 @@ import { TextInputSelectionChangeEventData, View, } from 'react-native' +import {AppBskyRichtextFacet, RichText} from '@atproto/api' import PasteInput, { PastedFile, PasteInputRef, } from '@mattermost/react-native-paste-input' -import {AppBskyRichtextFacet, RichText} from '@atproto/api' -import isEqual from 'lodash.isequal' -import {Autocomplete} from './mobile/Autocomplete' -import {Text} from 'view/com/util/text/Text' + +import {POST_IMG_MAX} from 'lib/constants' +import {usePalette} from 'lib/hooks/usePalette' +import {downloadAndResize} from 'lib/media/manip' +import {isUriImage} from 'lib/media/util' import {cleanError} from 'lib/strings/errors' import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip' -import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' -import {isUriImage} from 'lib/media/util' -import {downloadAndResize} from 'lib/media/manip' -import {POST_IMG_MAX} from 'lib/constants' import {isIOS} from 'platform/detection' +import { + LinkFacetMatch, + suggestLinkCardUri, +} from 'view/com/composer/text-input/text-input-util' +import {Text} from 'view/com/util/text/Text' +import {Autocomplete} from './mobile/Autocomplete' export interface TextInputRef { focus: () => void @@ -39,11 +43,10 @@ export interface TextInputRef { interface TextInputProps extends ComponentProps { richtext: RichText placeholder: string - suggestedLinks: Set setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void onPressPublish: (richtext: RichText) => Promise - onSuggestedLinksChanged: (uris: Set) => void + onNewLink: (uri: string) => void onError: (err: string) => void } @@ -56,10 +59,9 @@ export const TextInput = forwardRef(function TextInputImpl( { richtext, placeholder, - suggestedLinks, setRichText, onPhotoPasted, - onSuggestedLinksChanged, + onNewLink, onError, ...props }: TextInputProps, @@ -70,6 +72,7 @@ export const TextInput = forwardRef(function TextInputImpl( const textInputSelection = useRef({start: 0, end: 0}) const theme = useTheme() const [autocompletePrefix, setAutocompletePrefix] = useState('') + const prevLength = React.useRef(richtext.length) React.useImperativeHandle(ref, () => ({ focus: () => textInput.current?.focus(), @@ -79,6 +82,8 @@ export const TextInput = forwardRef(function TextInputImpl( getCursorPosition: () => undefined, // Not implemented on native })) + const pastSuggestedUris = useRef(new Set()) + const prevDetectedUris = useRef(new Map()) const onChangeText = useCallback( (newText: string) => { /* @@ -92,6 +97,8 @@ export const TextInput = forwardRef(function TextInputImpl( * @see https://github.com/bluesky-social/social-app/issues/929 */ setTimeout(async () => { + const mayBePaste = newText.length > prevLength.current + 1 + const newRt = new RichText({text: newText}) newRt.detectFacetsWithoutResolution() setRichText(newRt) @@ -106,8 +113,7 @@ export const TextInput = forwardRef(function TextInputImpl( setAutocompletePrefix('') } - const set: Set = new Set() - + const nextDetectedUris = new Map() if (newRt.facets) { for (const facet of newRt.facets) { for (const feature of facet.features) { @@ -126,26 +132,26 @@ export const TextInput = forwardRef(function TextInputImpl( onPhotoPasted(res.path) } } else { - set.add(feature.uri) + nextDetectedUris.set(feature.uri, {facet, rt: newRt}) } } } } } - - if (!isEqual(set, suggestedLinks)) { - onSuggestedLinksChanged(set) + const suggestedUri = suggestLinkCardUri( + mayBePaste, + nextDetectedUris, + prevDetectedUris.current, + pastSuggestedUris.current, + ) + prevDetectedUris.current = nextDetectedUris + if (suggestedUri) { + onNewLink(suggestedUri) } + prevLength.current = newText.length }, 1) }, - [ - setRichText, - autocompletePrefix, - setAutocompletePrefix, - suggestedLinks, - onSuggestedLinksChanged, - onPhotoPasted, - ], + [setRichText, autocompletePrefix, onPhotoPasted, onNewLink], ) const onPaste = useCallback( diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index c62d11201f..7f8dc2ed5e 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -1,28 +1,32 @@ -import React from 'react' +import React, {useRef} from 'react' import {StyleSheet, View} from 'react-native' -import {RichText, AppBskyRichtextFacet} from '@atproto/api' -import EventEmitter from 'eventemitter3' -import {useEditor, EditorContent, JSONContent} from '@tiptap/react' +import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' +import {AppBskyRichtextFacet, RichText} from '@atproto/api' +import {Trans} from '@lingui/macro' import {Document} from '@tiptap/extension-document' -import History from '@tiptap/extension-history' import Hardbreak from '@tiptap/extension-hard-break' +import History from '@tiptap/extension-history' import {Mention} from '@tiptap/extension-mention' import {Paragraph} from '@tiptap/extension-paragraph' import {Placeholder} from '@tiptap/extension-placeholder' import {Text as TiptapText} from '@tiptap/extension-text' -import isEqual from 'lodash.isequal' -import {createSuggestion} from './web/Autocomplete' -import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' -import {isUriImage, blobToDataUri} from 'lib/media/util' -import {Emoji} from './web/EmojiPicker.web' -import {LinkDecorator} from './web/LinkDecorator' import {generateJSON} from '@tiptap/html' -import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' +import {EditorContent, JSONContent, useEditor} from '@tiptap/react' +import EventEmitter from 'eventemitter3' + import {usePalette} from '#/lib/hooks/usePalette' +import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' +import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' +import {blobToDataUri, isUriImage} from 'lib/media/util' +import { + LinkFacetMatch, + suggestLinkCardUri, +} from 'view/com/composer/text-input/text-input-util' import {Portal} from '#/components/Portal' import {Text} from '../../util/text/Text' -import {Trans} from '@lingui/macro' -import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' +import {createSuggestion} from './web/Autocomplete' +import {Emoji} from './web/EmojiPicker.web' +import {LinkDecorator} from './web/LinkDecorator' import {TagDecorator} from './web/TagDecorator' export interface TextInputRef { @@ -38,7 +42,7 @@ interface TextInputProps { setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void onPressPublish: (richtext: RichText) => Promise - onSuggestedLinksChanged: (uris: Set) => void + onNewLink: (uri: string) => void onError: (err: string) => void } @@ -48,17 +52,15 @@ export const TextInput = React.forwardRef(function TextInputImpl( { richtext, placeholder, - suggestedLinks, setRichText, onPhotoPasted, onPressPublish, - onSuggestedLinksChanged, + onNewLink, }: // onError, TODO TextInputProps, ref, ) { const autocomplete = useActorAutocompleteFn() - const pal = usePalette('default') const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') @@ -139,6 +141,8 @@ export const TextInput = React.forwardRef(function TextInputImpl( } }, [setIsDropping]) + const pastSuggestedUris = useRef(new Set()) + const prevDetectedUris = useRef(new Map()) const editor = useEditor( { extensions, @@ -180,25 +184,33 @@ export const TextInput = React.forwardRef(function TextInputImpl( }, onUpdate({editor: editorProp}) { const json = editorProp.getJSON() + const newText = editorJsonToText(json) + const isPaste = window.event?.type === 'paste' - const newRt = new RichText({text: editorJsonToText(json).trimEnd()}) + const newRt = new RichText({text: newText}) newRt.detectFacetsWithoutResolution() setRichText(newRt) - const set: Set = new Set() - + const nextDetectedUris = new Map() if (newRt.facets) { for (const facet of newRt.facets) { for (const feature of facet.features) { if (AppBskyRichtextFacet.isLink(feature)) { - set.add(feature.uri) + nextDetectedUris.set(feature.uri, {facet, rt: newRt}) } } } } - if (!isEqual(set, suggestedLinks)) { - onSuggestedLinksChanged(set) + const suggestedUri = suggestLinkCardUri( + isPaste, + nextDetectedUris, + prevDetectedUris.current, + pastSuggestedUris.current, + ) + prevDetectedUris.current = nextDetectedUris + if (suggestedUri) { + onNewLink(suggestedUri) } }, }, @@ -256,15 +268,29 @@ export const TextInput = React.forwardRef(function TextInputImpl( ) }) -function editorJsonToText(json: JSONContent): string { +function editorJsonToText( + json: JSONContent, + isLastDocumentChild: boolean = false, +): string { let text = '' - if (json.type === 'doc' || json.type === 'paragraph') { + if (json.type === 'doc') { + if (json.content?.length) { + for (let i = 0; i < json.content.length; i++) { + const node = json.content[i] + const isLastNode = i === json.content.length - 1 + text += editorJsonToText(node, isLastNode) + } + } + } else if (json.type === 'paragraph') { if (json.content?.length) { - for (const node of json.content) { + for (let i = 0; i < json.content.length; i++) { + const node = json.content[i] text += editorJsonToText(node) } } - text += '\n' + if (!isLastDocumentChild) { + text += '\n' + } } else if (json.type === 'hardBreak') { text += '\n' } else if (json.type === 'text') { diff --git a/src/view/com/composer/text-input/text-input-util.ts b/src/view/com/composer/text-input/text-input-util.ts new file mode 100644 index 0000000000..cbe8ef6af7 --- /dev/null +++ b/src/view/com/composer/text-input/text-input-util.ts @@ -0,0 +1,92 @@ +import {AppBskyRichtextFacet, RichText} from '@atproto/api' + +export type LinkFacetMatch = { + rt: RichText + facet: AppBskyRichtextFacet.Main +} + +export function suggestLinkCardUri( + mayBePaste: boolean, + nextDetectedUris: Map, + prevDetectedUris: Map, + pastSuggestedUris: Set, +): string | undefined { + const suggestedUris = new Set() + for (const [uri, nextMatch] of nextDetectedUris) { + if (!isValidUrlAndDomain(uri)) { + continue + } + if (pastSuggestedUris.has(uri)) { + // Don't suggest already added or already dismissed link cards. + continue + } + if (mayBePaste) { + // Immediately add the pasted link without waiting to type more. + suggestedUris.add(uri) + continue + } + const prevMatch = prevDetectedUris.get(uri) + if (!prevMatch) { + // If the same exact link wasn't already detected during the last keystroke, + // it means you're probably still typing it. Disregard until it stabilizes. + continue + } + const prevTextAfterUri = prevMatch.rt.unicodeText.slice( + prevMatch.facet.index.byteEnd, + ) + const nextTextAfterUri = nextMatch.rt.unicodeText.slice( + nextMatch.facet.index.byteEnd, + ) + if (prevTextAfterUri === nextTextAfterUri) { + // The text you're editing is before the link, e.g. + // "abc google.com" -> "abcd google.com". + // This is a good time to add the link. + suggestedUris.add(uri) + continue + } + if (/^\s/m.test(nextTextAfterUri)) { + // The link is followed by a space, e.g. + // "google.com" -> "google.com " or + // "google.com." -> "google.com ". + // This is a clear indicator we can linkify it. + suggestedUris.add(uri) + continue + } + if ( + /^[)]?[.,:;!?)](\s|$)/m.test(prevTextAfterUri) && + /^[)]?[.,:;!?)]\s/m.test(nextTextAfterUri) + ) { + // The link was *already* being followed by punctuation, + // and now it's followed both by punctuation and a space. + // This means you're typing after punctuation, e.g. + // "google.com." -> "google.com. " or + // "google.com.foo" -> "google.com. foo". + // This means you're not typing the link anymore, so we can linkify it. + suggestedUris.add(uri) + continue + } + } + for (const uri of pastSuggestedUris) { + if (!nextDetectedUris.has(uri)) { + // If a link is no longer detected, it's eligible for suggestions next time. + pastSuggestedUris.delete(uri) + } + } + + let suggestedUri: string | undefined + if (suggestedUris.size > 0) { + suggestedUri = Array.from(suggestedUris)[0] + pastSuggestedUris.add(suggestedUri) + } + + return suggestedUri +} + +// https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url +// question credit Muhammad Imran Tariq https://stackoverflow.com/users/420613/muhammad-imran-tariq +// answer credit Christian David https://stackoverflow.com/users/967956/christian-david +function isValidUrlAndDomain(value: string) { + return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( + value, + ) +} diff --git a/src/view/com/composer/text-input/web/LinkDecorator.ts b/src/view/com/composer/text-input/web/LinkDecorator.ts index e36ac80e42..60f2d40855 100644 --- a/src/view/com/composer/text-input/web/LinkDecorator.ts +++ b/src/view/com/composer/text-input/web/LinkDecorator.ts @@ -14,11 +14,11 @@ * the facet-set. */ +import {URL_REGEX} from '@atproto/api' import {Mark} from '@tiptap/core' -import {Plugin, PluginKey} from '@tiptap/pm/state' import {Node as ProsemirrorNode} from '@tiptap/pm/model' +import {Plugin, PluginKey} from '@tiptap/pm/state' import {Decoration, DecorationSet} from '@tiptap/pm/view' -import {URL_REGEX} from '@atproto/api' import {isValidDomain} from 'lib/strings/url-helpers' @@ -91,7 +91,7 @@ function iterateUris(str: string, cb: (from: number, to: number) => void) { uri = `https://${uri}` } let from = str.indexOf(match[2], match.index) - let to = from + match[2].length + 1 + let to = from + match[2].length // strip ending puncuation if (/[.,;!?]$/.test(uri)) { uri = uri.slice(0, -1) diff --git a/src/view/com/composer/useExternalLinkFetch.e2e.ts b/src/view/com/composer/useExternalLinkFetch.e2e.ts index ccf619db37..65ecb866e7 100644 --- a/src/view/com/composer/useExternalLinkFetch.e2e.ts +++ b/src/view/com/composer/useExternalLinkFetch.e2e.ts @@ -1,12 +1,14 @@ -import {useState, useEffect} from 'react' +import {useEffect, useState} from 'react' + +import {useAgent} from '#/state/session' import * as apilib from 'lib/api/index' import {getLinkMeta} from 'lib/link-meta/link-meta' import {ComposerOpts} from 'state/shell/composer' -import {getAgent} from '#/state/session' export function useExternalLinkFetch({}: { setQuote: (opts: ComposerOpts['quote']) => void }) { + const {getAgent} = useAgent() const [extLink, setExtLink] = useState( undefined, ) @@ -39,7 +41,7 @@ export function useExternalLinkFetch({}: { }) } return cleanup - }, [extLink]) + }, [extLink, getAgent]) return {extLink, setExtLink} } diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts index 54773d565c..d51dec42b1 100644 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ b/src/view/com/composer/useExternalLinkFetch.ts @@ -1,24 +1,25 @@ -import {useState, useEffect} from 'react' -import {ImageModel} from 'state/models/media/image' +import {useEffect, useState} from 'react' + +import {logger} from '#/logger' +import {useFetchDid} from '#/state/queries/handle' +import {useGetPost} from '#/state/queries/post' +import {useAgent} from '#/state/session' import * as apilib from 'lib/api/index' -import {getLinkMeta} from 'lib/link-meta/link-meta' +import {POST_IMG_MAX} from 'lib/constants' import { - getPostAsQuote, getFeedAsEmbed, getListAsEmbed, + getPostAsQuote, } from 'lib/link-meta/bsky' +import {getLinkMeta} from 'lib/link-meta/link-meta' import {downloadAndResize} from 'lib/media/manip' import { - isBskyPostUrl, isBskyCustomFeedUrl, isBskyListUrl, + isBskyPostUrl, } from 'lib/strings/url-helpers' +import {ImageModel} from 'state/models/media/image' import {ComposerOpts} from 'state/shell/composer' -import {POST_IMG_MAX} from 'lib/constants' -import {logger} from '#/logger' -import {getAgent} from '#/state/session' -import {useGetPost} from '#/state/queries/post' -import {useFetchDid} from '#/state/queries/handle' export function useExternalLinkFetch({ setQuote, @@ -30,6 +31,7 @@ export function useExternalLinkFetch({ ) const getPost = useGetPost() const fetchDid = useFetchDid() + const {getAgent} = useAgent() useEffect(() => { let aborted = false @@ -135,7 +137,7 @@ export function useExternalLinkFetch({ }) } return cleanup - }, [extLink, setQuote, getPost, fetchDid]) + }, [extLink, setQuote, getPost, fetchDid, getAgent]) return {extLink, setExtLink} } diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 25c7e1006d..4ebf64da9a 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -53,6 +53,7 @@ export function FeedPage({ const headerOffset = useHeaderOffset() const scrollElRef = React.useRef(null) const [hasNew, setHasNew] = React.useState(false) + const gate = useGate() const scrollToTop = React.useCallback(() => { scrollElRef.current?.scrollToOffset({ @@ -103,16 +104,11 @@ export function FeedPage({ }) }, [scrollToTop, feed, queryClient, setHasNew]) - let feedPollInterval - if ( - useGate('disable_poll_on_discover') && - feed === // Discover - 'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' - ) { - feedPollInterval = undefined - } else { - feedPollInterval = POLL_FREQ - } + const isDiscoverFeed = + feed === + 'feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' + const adjustedHasNew = + hasNew && !(isDiscoverFeed && gate('disable_poll_on_discover_v2')) return ( @@ -122,7 +118,7 @@ export function FeedPage({ enabled={isPageFocused} feed={feed} feedParams={feedParams} - pollInterval={feedPollInterval} + pollInterval={POLL_FREQ} disablePoll={hasNew} scrollElRef={scrollElRef} onScrolledDownChange={setIsScrolledDown} @@ -132,11 +128,11 @@ export function FeedPage({ headerOffset={headerOffset} /> - {(isScrolledDown || hasNew) && ( + {(isScrolledDown || adjustedHasNew) && ( )} diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index e9cf9e5359..a006b11c06 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -1,22 +1,29 @@ import React from 'react' -import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import { + findNodeHandle, + StyleProp, + StyleSheet, + View, + ViewStyle, +} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {List, ListRef} from '../util/List' -import {FeedSourceCardLoaded} from './FeedSourceCard' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {Text} from '../util/text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens' -import {logger} from '#/logger' -import {Trans, msg} from '@lingui/macro' + import {cleanError} from '#/lib/strings/errors' import {useTheme} from '#/lib/ThemeContext' -import {usePreferencesQuery} from '#/state/queries/preferences' +import {logger} from '#/logger' +import {isNative} from '#/platform/detection' import {hydrateFeedGenerator} from '#/state/queries/feed' +import {usePreferencesQuery} from '#/state/queries/preferences' +import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' +import {usePalette} from 'lib/hooks/usePalette' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import {isNative} from '#/platform/detection' -import {useLingui} from '@lingui/react' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {List, ListRef} from '../util/List' +import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' +import {Text} from '../util/text/Text' +import {FeedSourceCardLoaded} from './FeedSourceCard' const LOADING = {_reactKey: '__loading__'} const EMPTY = {_reactKey: '__empty__'} @@ -34,13 +41,14 @@ interface ProfileFeedgensProps { enabled?: boolean style?: StyleProp testID?: string + setScrollViewTag: (tag: number | null) => void } export const ProfileFeedgens = React.forwardRef< SectionRef, ProfileFeedgensProps >(function ProfileFeedgensImpl( - {did, scrollElRef, headerOffset, enabled, style, testID}, + {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, ref, ) { const pal = usePalette('default') @@ -169,6 +177,13 @@ export const ProfileFeedgens = React.forwardRef< [error, refetch, onPressRetryLoadMore, pal, preferences, _], ) + React.useEffect(() => { + if (enabled && scrollElRef.current) { + const nativeTag = findNodeHandle(scrollElRef.current) + setScrollViewTag(nativeTag) + } + }, [enabled, scrollElRef, setScrollViewTag]) + return ( - - - - - - - - - + {hasSession && ( + + + + + + + + + + )} {tabBarAnchor} { diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index d7b7231c60..78fa9af865 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -1,23 +1,24 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {usePalette} from 'lib/hooks/usePalette' -import {Link} from '../util/Link' +import Animated from 'react-native-reanimated' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' -import {HITSLOP_10} from 'lib/constants' -import Animated from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' + +import {useSession} from '#/state/session' import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useShellLayout} from '#/state/shell/shell-layout' +import {HITSLOP_10} from 'lib/constants' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {Logo} from '#/view/icons/Logo' - -import {IS_DEV} from '#/env' import {atoms} from '#/alf' -import {Link as Link2} from '#/components/Link' import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' +import {Link as Link2} from '#/components/Link' +import {IS_DEV} from '#/env' +import {Link} from '../util/Link' export function HomeHeaderLayoutMobile({ children, @@ -30,6 +31,7 @@ export function HomeHeaderLayoutMobile({ const setDrawerOpen = useSetDrawerOpen() const {headerHeight} = useShellLayout() const {headerMinimalShellTransform} = useMinimalShellMode() + const {hasSession} = useSession() const onPressAvi = React.useCallback(() => { setDrawerOpen(true) @@ -76,18 +78,20 @@ export function HomeHeaderLayoutMobile({ )} - - - + {hasSession && ( + + + + )} {children} diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index a47b25bed4..003d1c60e7 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -1,21 +1,28 @@ import React from 'react' -import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import { + findNodeHandle, + StyleProp, + StyleSheet, + View, + ViewStyle, +} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {List, ListRef} from '../util/List' -import {ListCard} from './ListCard' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' -import {Text} from '../util/text/Text' -import {useAnalytics} from 'lib/analytics/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists' -import {logger} from '#/logger' -import {Trans, msg} from '@lingui/macro' + import {cleanError} from '#/lib/strings/errors' import {useTheme} from '#/lib/ThemeContext' -import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {logger} from '#/logger' import {isNative} from '#/platform/detection' -import {useLingui} from '@lingui/react' +import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' +import {useAnalytics} from 'lib/analytics/analytics' +import {usePalette} from 'lib/hooks/usePalette' +import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {List, ListRef} from '../util/List' +import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' +import {Text} from '../util/text/Text' +import {ListCard} from './ListCard' const LOADING = {_reactKey: '__loading__'} const EMPTY = {_reactKey: '__empty__'} @@ -33,11 +40,12 @@ interface ProfileListsProps { enabled?: boolean style?: StyleProp testID?: string + setScrollViewTag: (tag: number | null) => void } export const ProfileLists = React.forwardRef( function ProfileListsImpl( - {did, scrollElRef, headerOffset, enabled, style, testID}, + {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, ref, ) { const pal = usePalette('default') @@ -171,6 +179,13 @@ export const ProfileLists = React.forwardRef( [error, refetch, onPressRetryLoadMore, pal, _], ) + React.useEffect(() => { + if (enabled && scrollElRef.current) { + const nativeTag = findNodeHandle(scrollElRef.current) + setScrollViewTag(nativeTag) + } + }, [enabled, scrollElRef, setScrollViewTag]) + return ( (Stages.InputEmail) diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx index 125da44be7..ae43d1e328 100644 --- a/src/view/com/modals/ChangeHandle.tsx +++ b/src/view/com/modals/ChangeHandle.tsx @@ -16,8 +16,8 @@ import {useModalControls} from '#/state/modals' import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle' import {useServiceQuery} from '#/state/queries/service' import { - getAgent, SessionAccount, + useAgent, useSession, useSessionApi, } from '#/state/session' @@ -40,6 +40,7 @@ export type Props = {onChanged: () => void} export function Component(props: Props) { const {currentAccount} = useSession() + const {getAgent} = useAgent() const { isLoading, data: serviceInfo, diff --git a/src/view/com/modals/ChangePassword.tsx b/src/view/com/modals/ChangePassword.tsx index 4badc88aaa..3ce7306b9d 100644 --- a/src/view/com/modals/ChangePassword.tsx +++ b/src/view/com/modals/ChangePassword.tsx @@ -6,24 +6,25 @@ import { TouchableOpacity, View, } from 'react-native' -import {ScrollView} from './util' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {TextInput} from './util' -import {Text} from '../util/text/Text' -import {Button} from '../util/forms/Button' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {s, colors} from 'lib/styles' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import * as EmailValidator from 'email-validator' + +import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' +import {useAgent, useSession} from '#/state/session' import {usePalette} from 'lib/hooks/usePalette' -import {isAndroid, isWeb} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {cleanError, isNetworkError} from 'lib/strings/errors' import {checkAndFormatResetCode} from 'lib/strings/password' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' -import {useSession, getAgent} from '#/state/session' -import * as EmailValidator from 'email-validator' -import {logger} from '#/logger' +import {colors, s} from 'lib/styles' +import {isAndroid, isWeb} from 'platform/detection' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {Button} from '../util/forms/Button' +import {Text} from '../util/text/Text' +import {ScrollView} from './util' +import {TextInput} from './util' enum Stages { RequestCode, @@ -36,6 +37,7 @@ export const snapPoints = isAndroid ? ['90%'] : ['45%'] export function Component() { const pal = usePalette('default') const {currentAccount} = useSession() + const {getAgent} = useAgent() const {_} = useLingui() const [stage, setStage] = useState(Stages.RequestCode) const [isProcessing, setIsProcessing] = useState(false) diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx index f5f4f56db0..2dff636afc 100644 --- a/src/view/com/modals/CreateOrEditList.tsx +++ b/src/view/com/modals/CreateOrEditList.tsx @@ -25,7 +25,7 @@ import { useListCreateMutation, useListMetadataMutation, } from '#/state/queries/list' -import {getAgent} from '#/state/session' +import {useAgent} from '#/state/session' import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' @@ -62,6 +62,7 @@ export function Component({ const {_} = useLingui() const listCreateMutation = useListCreateMutation() const listMetadataMutation = useListMetadataMutation() + const {getAgent} = useAgent() const activePurpose = useMemo(() => { if (list?.purpose) { @@ -228,6 +229,7 @@ export function Component({ listMetadataMutation, listCreateMutation, _, + getAgent, ]) return ( diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx index 4c4fb20f18..5e68daef9a 100644 --- a/src/view/com/modals/DeleteAccount.tsx +++ b/src/view/com/modals/DeleteAccount.tsx @@ -11,7 +11,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' -import {getAgent, useSession, useSessionApi} from '#/state/session' +import {useAgent, useSession, useSessionApi} from '#/state/session' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {cleanError} from 'lib/strings/errors' @@ -30,6 +30,7 @@ export function Component({}: {}) { const pal = usePalette('default') const theme = useTheme() const {currentAccount} = useSession() + const {getAgent} = useAgent() const {clearCurrentAccount, removeAccount} = useSessionApi() const {_} = useLingui() const {closeModal} = useModalControls() diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx index d3086d3831..d6a3006cc9 100644 --- a/src/view/com/modals/VerifyEmail.tsx +++ b/src/view/com/modals/VerifyEmail.tsx @@ -6,23 +6,24 @@ import { StyleSheet, View, } from 'react-native' -import {Svg, Circle, Path} from 'react-native-svg' -import {ScrollView, TextInput} from './util' +import {Circle, Path, Svg} from 'react-native-svg' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Text} from '../util/text/Text' -import {Button} from '../util/forms/Button' -import {ErrorMessage} from '../util/error/ErrorMessage' -import * as Toast from '../util/Toast' -import {s, colors} from 'lib/styles' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' +import {useAgent, useSession, useSessionApi} from '#/state/session' import {usePalette} from 'lib/hooks/usePalette' -import {isWeb} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {cleanError} from 'lib/strings/errors' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' -import {useSession, useSessionApi, getAgent} from '#/state/session' -import {logger} from '#/logger' +import {colors, s} from 'lib/styles' +import {isWeb} from 'platform/detection' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {Button} from '../util/forms/Button' +import {Text} from '../util/text/Text' +import * as Toast from '../util/Toast' +import {ScrollView, TextInput} from './util' export const snapPoints = ['90%'] @@ -32,8 +33,15 @@ enum Stages { ConfirmCode, } -export function Component({showReminder}: {showReminder?: boolean}) { +export function Component({ + showReminder, + onSuccess, +}: { + showReminder?: boolean + onSuccess?: () => void +}) { const pal = usePalette('default') + const {getAgent} = useAgent() const {currentAccount} = useSession() const {updateCurrentAccount} = useSessionApi() const {_} = useLingui() @@ -77,6 +85,7 @@ export function Component({showReminder}: {showReminder?: boolean}) { updateCurrentAccount({emailConfirmed: true}) Toast.show(_(msg`Email verified`)) closeModal() + onSuccess?.() } catch (e) { setError(cleanError(String(e))) } finally { diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 78b1677c3d..94844cb1a7 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -1,20 +1,20 @@ -import React, {memo, useMemo, useState, useEffect} from 'react' +import React, {memo, useEffect, useMemo, useState} from 'react' import { Animated, - TouchableOpacity, Pressable, StyleSheet, + TouchableOpacity, View, } from 'react-native' import { + AppBskyActorDefs, AppBskyEmbedImages, + AppBskyEmbedRecordWithMedia, AppBskyFeedDefs, AppBskyFeedPost, - ModerationOpts, - ModerationDecision, moderateProfile, - AppBskyEmbedRecordWithMedia, - AppBskyActorDefs, + ModerationDecision, + ModerationOpts, } from '@atproto/api' import {AtUri} from '@atproto/api' import { @@ -22,41 +22,41 @@ import { FontAwesomeIconStyle, Props, } from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' + import {FeedNotification} from '#/state/queries/notifications/feed' -import {s, colors} from 'lib/styles' -import {niceDate} from 'lib/strings/time' +import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {usePalette} from 'lib/hooks/usePalette' +import {HeartIconSolid} from 'lib/icons' +import {makeProfileLink} from 'lib/routes/links' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {pluralize} from 'lib/strings/helpers' -import {HeartIconSolid} from 'lib/icons' -import {Text} from '../util/text/Text' -import {UserAvatar, PreviewableUserAvatar} from '../util/UserAvatar' -import {UserPreviewLink} from '../util/UserPreviewLink' -import {ImageHorzList} from '../util/images/ImageHorzList' +import {niceDate} from 'lib/strings/time' +import {colors, s} from 'lib/styles' +import {isWeb} from 'platform/detection' +import {precacheProfile} from 'state/queries/profile' +import {Link as NewLink} from '#/components/Link' +import {ProfileHoverCard} from '#/components/ProfileHoverCard' +import {FeedSourceCard} from '../feeds/FeedSourceCard' import {Post} from '../post/Post' +import {ImageHorzList} from '../util/images/ImageHorzList' import {Link, TextLink} from '../util/Link' -import {usePalette} from 'lib/hooks/usePalette' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {formatCount} from '../util/numeric/format' -import {makeProfileLink} from 'lib/routes/links' +import {Text} from '../util/text/Text' import {TimeElapsed} from '../util/TimeElapsed' -import {isWeb} from 'platform/detection' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {FeedSourceCard} from '../feeds/FeedSourceCard' +import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar' const MAX_AUTHORS = 5 const EXPANDED_AUTHOR_EL_HEIGHT = 35 interface Author { + profile: AppBskyActorDefs.ProfileViewBasic href: string - did: string - handle: string - displayName?: string - avatar?: string moderation: ModerationDecision - associated?: AppBskyActorDefs.ProfileAssociated } let FeedItem = ({ @@ -66,6 +66,7 @@ let FeedItem = ({ item: FeedNotification moderationOpts: ModerationOpts }): React.ReactNode => { + const queryClient = useQueryClient() const pal = usePalette('default') const {_} = useLingui() const [isAuthorsExpanded, setAuthorsExpanded] = useState(false) @@ -93,28 +94,22 @@ let FeedItem = ({ setAuthorsExpanded(currentlyExpanded => !currentlyExpanded) } + const onBeforePress = React.useCallback(() => { + precacheProfile(queryClient, item.notification.author) + }, [queryClient, item.notification.author]) + const authors: Author[] = useMemo(() => { return [ { + profile: item.notification.author, href: makeProfileLink(item.notification.author), - did: item.notification.author.did, - handle: item.notification.author.handle, - displayName: item.notification.author.displayName, - avatar: item.notification.author.avatar, moderation: moderateProfile(item.notification.author, moderationOpts), - associated: item.notification.author.associated, }, - ...(item.additional?.map(({author}) => { - return { - href: makeProfileLink(author), - did: author.did, - handle: author.handle, - displayName: author.displayName, - avatar: author.avatar, - moderation: moderateProfile(author, moderationOpts), - associated: author.associated, - } - }) || []), + ...(item.additional?.map(({author}) => ({ + profile: author, + href: makeProfileLink(author), + moderation: moderateProfile(author, moderationOpts), + })) || []), ] }, [item, moderationOpts]) @@ -199,7 +194,8 @@ let FeedItem = ({ accessible={ (item.type === 'post-like' && authors.length === 1) || item.type === 'repost' - }> + } + onBeforePress={onBeforePress}> {/* TODO: Prevent conditional rendering and move toward composable notifications for clearer accessibility labeling */} @@ -229,7 +225,7 @@ let FeedItem = ({ style={[pal.text, s.bold]} href={authors[0].href} text={sanitizeDisplayName( - authors[0].displayName || authors[0].handle, + authors[0].profile.displayName || authors[0].profile.handle, )} disableMismatchWarning /> @@ -337,11 +333,9 @@ function CondensedAuthorsList({ ) @@ -356,11 +350,11 @@ function CondensedAuthorsList({ {authors.slice(0, MAX_AUTHORS).map(author => ( - ))} @@ -386,6 +380,7 @@ function ExpandedAuthorsList({ visible: boolean authors: Author[] }) { + const {_} = useLingui() const pal = usePalette('default') const heightInterp = useAnimatedValue(visible ? 1 : 0) const targetHeight = @@ -409,18 +404,23 @@ function ExpandedAuthorsList({ visible ? s.mb10 : undefined, ]}> {authors.map(author => ( - - + + + - {sanitizeDisplayName(author.displayName || author.handle)} + {sanitizeDisplayName( + author.profile.displayName || author.profile.handle, + )}   - {sanitizeHandle(author.handle)} + {sanitizeHandle(author.profile.handle)} - + ))} ) diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index aa110682a2..2d604d104e 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -1,26 +1,28 @@ import * as React from 'react' import { LayoutChangeEvent, + NativeScrollEvent, ScrollView, StyleSheet, View, - NativeScrollEvent, } from 'react-native' import Animated, { - useAnimatedStyle, - useSharedValue, + AnimatedRef, runOnJS, runOnUI, scrollTo, - useAnimatedRef, - AnimatedRef, SharedValue, + useAnimatedRef, + useAnimatedStyle, + useSharedValue, } from 'react-native-reanimated' -import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' -import {TabBar} from './TabBar' + import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {ListMethods} from '../util/List' import {ScrollProvider} from '#/lib/ScrollContext' +import {isIOS} from 'platform/detection' +import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' +import {ListMethods} from '../util/List' +import {TabBar} from './TabBar' export interface PagerWithHeaderChildParams { headerHeight: number @@ -236,9 +238,12 @@ let PagerTabBar = ({ const headerRef = React.useRef(null) return ( - + {renderHeader?.()} { // It wouldn't be enough to place `onLayout` on the parent node because diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/view/com/post-thread/PostThreadFollowBtn.tsx index 8b297121eb..1f70f41c4a 100644 --- a/src/view/com/post-thread/PostThreadFollowBtn.tsx +++ b/src/view/com/post-thread/PostThreadFollowBtn.tsx @@ -48,7 +48,7 @@ function PostThreadFollowBtnLoaded({ 'PostThreadItem', ) const requireAuth = useRequireAuth() - const showFollowBackLabel = useGate('show_follow_back_label') + const gate = useGate() const isFollowing = !!profile.viewer?.following const isFollowedBy = !!profile.viewer?.followedBy @@ -140,7 +140,7 @@ function PostThreadFollowBtnLoaded({ style={[!isFollowing ? palInverted.text : pal.text, s.bold]} numberOfLines={1}> {!isFollowing ? ( - showFollowBackLabel && isFollowedBy ? ( + isFollowedBy && gate('show_follow_back_label_v2') ? ( Follow Back ) : ( Follow diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 6555bdf73c..564e37e7a6 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -1,50 +1,51 @@ import React, {memo, useMemo} from 'react' import {StyleSheet, View} from 'react-native' import { - AtUri, AppBskyFeedDefs, AppBskyFeedPost, - RichText as RichTextAPI, + AtUri, ModerationDecision, + RichText as RichTextAPI, } from '@atproto/api' -import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' -import {Link, TextLink} from '../util/Link' -import {RichText} from '#/components/RichText' -import {Text} from '../util/text/Text' -import {PreviewableUserAvatar} from '../util/UserAvatar' -import {s} from 'lib/styles' -import {niceDate} from 'lib/strings/time' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' +import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' +import {useLanguagePrefs} from '#/state/preferences' +import {useOpenLink} from '#/state/preferences/in-app-browser' +import {ThreadPost} from '#/state/queries/post-thread' +import {useModerationOpts} from '#/state/queries/preferences' +import {useComposerControls} from '#/state/shell/composer' +import {MAX_POST_LINES} from 'lib/constants' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {makeProfileLink} from 'lib/routes/links' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {countLines, pluralize} from 'lib/strings/helpers' -import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' -import {PostMeta} from '../util/PostMeta' -import {PostEmbeds} from '../util/post-embeds' -import {PostCtrls} from '../util/post-ctrls/PostCtrls' -import {PostHider} from '../../../components/moderation/PostHider' +import {niceDate} from 'lib/strings/time' +import {s} from 'lib/styles' +import {isWeb} from 'platform/detection' +import {useSession} from 'state/session' +import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' +import {atoms as a} from '#/alf' +import {RichText} from '#/components/RichText' import {ContentHider} from '../../../components/moderation/ContentHider' -import {PostAlerts} from '../../../components/moderation/PostAlerts' 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 {ErrorMessage} from '../util/error/ErrorMessage' -import {usePalette} from 'lib/hooks/usePalette' +import {Link, TextLink} from '../util/Link' import {formatCount} from '../util/numeric/format' -import {makeProfileLink} from 'lib/routes/links' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {MAX_POST_LINES} from 'lib/constants' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useLanguagePrefs} from '#/state/preferences' -import {useComposerControls} from '#/state/shell/composer' -import {useModerationOpts} from '#/state/queries/preferences' -import {useOpenLink} from '#/state/preferences/in-app-browser' -import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' -import {ThreadPost} from '#/state/queries/post-thread' -import {useSession} from 'state/session' -import {WhoCanReply} from '../threadgate/WhoCanReply' -import {LoadingPlaceholder} from '../util/LoadingPlaceholder' -import {atoms as a} from '#/alf' +import {PostCtrls} from '../util/post-ctrls/PostCtrls' +import {PostEmbeds} from '../util/post-embeds' +import {PostMeta} from '../util/PostMeta' +import {Text} from '../util/text/Text' +import {PreviewableUserAvatar} from '../util/UserAvatar' export function PostThreadItem({ post, @@ -248,9 +249,7 @@ let PostThreadItemLoaded = ({ @@ -325,12 +324,6 @@ let PostThreadItemLoaded = ({ {post.repostCount !== 0 || post.likeCount !== 0 ? ( // Show this section unless we're *sure* it has no engagement. - {post.repostCount == null && post.likeCount == null && ( - // If we're still loading and not sure, assume this post has engagement. - // This lets us avoid a layout shift for the common case (embedded post with likes/reposts). - // TODO: embeds should include metrics to avoid us having to guess. - - )} {post.repostCount != null && post.repostCount !== 0 ? ( + } + profile={post.author}> @@ -484,7 +476,12 @@ let PostThreadItemLoaded = ({ avatarSize={28} displayNameType="md-bold" displayNameStyle={isThreadedChild && s.ml2} - style={isThreadedChild && s.mb2} + style={ + isThreadedChild && { + alignItems: 'center', + paddingBottom: isWeb ? 5 : 2, + } + } /> }) { + const queryClient = useQueryClient() const pal = usePalette('default') const {_} = useLingui() const {openComposer} = useComposerControls() @@ -129,16 +134,21 @@ function PostInner({ setLimitLines(false) }, [setLimitLines]) + const onBeforePress = React.useCallback(() => { + precacheProfile(queryClient, post.author) + }, [queryClient, post.author]) + return ( - + {showReplyLine && } @@ -165,12 +175,14 @@ function PostInner({ numberOfLines={1}> Reply to{' '} - + + + diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 0fbcc4a13c..605dffde9d 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -11,31 +11,35 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {ReasonFeedSource, isReasonFeedSource} from 'lib/api/feed/types' -import {Link, TextLinkOnWebOnly, TextLink} from '../util/Link' -import {Text} from '../util/text/Text' -import {UserInfoText} from '../util/UserInfoText' -import {PostMeta} from '../util/PostMeta' -import {PostCtrls} from '../util/post-ctrls/PostCtrls' -import {PostEmbeds} from '../util/post-embeds' -import {ContentHider} from '#/components/moderation/ContentHider' -import {PostAlerts} from '../../../components/moderation/PostAlerts' -import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' -import {RichText} from '#/components/RichText' -import {PreviewableUserAvatar} from '../util/UserAvatar' -import {s} from 'lib/styles' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' + +import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' +import {useComposerControls} from '#/state/shell/composer' +import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types' +import {MAX_POST_LINES} from 'lib/constants' import {usePalette} from 'lib/hooks/usePalette' +import {makeProfileLink} from 'lib/routes/links' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' -import {makeProfileLink} from 'lib/routes/links' -import {MAX_POST_LINES} from 'lib/constants' import {countLines} from 'lib/strings/helpers' -import {useComposerControls} from '#/state/shell/composer' -import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' -import {FeedNameText} from '../util/FeedInfoText' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' +import {s} from 'lib/styles' +import {precacheProfile} from 'state/queries/profile' import {atoms as a} from '#/alf' +import {ContentHider} from '#/components/moderation/ContentHider' +import {ProfileHoverCard} from '#/components/ProfileHoverCard' +import {RichText} from '#/components/RichText' +import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' +import {PostAlerts} from '../../../components/moderation/PostAlerts' +import {FeedNameText} from '../util/FeedInfoText' +import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link' +import {PostCtrls} from '../util/post-ctrls/PostCtrls' +import {PostEmbeds} from '../util/post-embeds' +import {PostMeta} from '../util/PostMeta' +import {Text} from '../util/text/Text' +import {PreviewableUserAvatar} from '../util/UserAvatar' +import {UserInfoText} from '../util/UserInfoText' export function FeedItem({ post, @@ -104,6 +108,7 @@ let FeedItemInner = ({ isThreadLastChild?: boolean isThreadParent?: boolean }): React.ReactNode => { + const queryClient = useQueryClient() const {openComposer} = useComposerControls() const pal = usePalette('default') const {_} = useLingui() @@ -133,6 +138,10 @@ let FeedItemInner = ({ }) }, [post, record, openComposer, moderation]) + const onBeforePress = React.useCallback(() => { + precacheProfile(queryClient, post.author) + }, [queryClient, post.author]) + const outerStyles = [ styles.outer, { @@ -151,7 +160,8 @@ let FeedItemInner = ({ style={outerStyles} href={href} noFeedback - accessible={false}> + accessible={false} + onBeforePress={onBeforePress}> {isThreadChild && ( @@ -213,17 +223,20 @@ let FeedItemInner = ({ numberOfLines={1}> Reposted by{' '} - + + + @@ -235,9 +248,7 @@ let FeedItemInner = ({ @@ -279,12 +290,14 @@ let FeedItemInner = ({ numberOfLines={1}> Reply to{' '} - + + + diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 235139fff0..90ab9b7385 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import { AppBskyActorDefs, @@ -6,22 +6,25 @@ import { ModerationCause, ModerationDecision, } from '@atproto/api' -import {Link} from '../util/Link' -import {Text} from '../util/text/Text' -import {UserAvatar} from '../util/UserAvatar' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {FollowButton} from './FollowButton' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {makeProfileLink} from 'lib/routes/links' -import {getModerationCauseKey, isJustAMute} from 'lib/moderation' +import {Trans} from '@lingui/macro' +import {useQueryClient} from '@tanstack/react-query' + +import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' +import {useProfileShadow} from '#/state/cache/profile-shadow' import {Shadow} from '#/state/cache/types' import {useModerationOpts} from '#/state/queries/preferences' -import {useProfileShadow} from '#/state/cache/profile-shadow' import {useSession} from '#/state/session' -import {Trans} from '@lingui/macro' -import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' +import {usePalette} from 'lib/hooks/usePalette' +import {getModerationCauseKey, isJustAMute} from 'lib/moderation' +import {makeProfileLink} from 'lib/routes/links' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' +import {s} from 'lib/styles' +import {precacheProfile} from 'state/queries/profile' +import {Link} from '../util/Link' +import {Text} from '../util/text/Text' +import {PreviewableUserAvatar} from '../util/UserAvatar' +import {FollowButton} from './FollowButton' export function ProfileCard({ testID, @@ -46,10 +49,17 @@ export function ProfileCard({ onPress?: () => void style?: StyleProp }) { + const queryClient = useQueryClient() const pal = usePalette('default') const profile = useProfileShadow(profileUnshadowed) const moderationOpts = useModerationOpts() const isLabeler = profile?.associated?.labeler + + const onBeforePress = React.useCallback(() => { + onPress?.() + precacheProfile(queryClient, profile) + }, [onPress, profile, queryClient]) + if (!moderationOpts) { return null } @@ -71,14 +81,14 @@ export function ProfileCard({ ]} href={makeProfileLink(profile)} title={profile.handle} - onBeforePress={onPress} asAnchor + onBeforePress={onBeforePress} anchorNoUnderline> - @@ -221,9 +231,9 @@ function FollowersList({ {followersWithMods.slice(0, 3).map(({f, mod}) => ( - diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx index 3602cdb9a8..4c9d164f75 100644 --- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx +++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx @@ -1,28 +1,28 @@ import React from 'react' -import {View, StyleSheet, Pressable, ScrollView} from 'react-native' +import {Pressable, ScrollView, StyleSheet, View} from 'react-native' import {AppBskyActorDefs, moderateProfile} from '@atproto/api' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' -import * as Toast from '../util/Toast' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import {useModerationOpts} from '#/state/queries/preferences' +import {useProfileFollowMutationQueue} from '#/state/queries/profile' +import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' +import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' -import {Text} from 'view/com/util/text/Text' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {Button} from 'view/com/util/forms/Button' +import {makeProfileLink} from 'lib/routes/links' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' -import {makeProfileLink} from 'lib/routes/links' -import {Link} from 'view/com/util/Link' -import {useAnalytics} from 'lib/analytics/analytics' import {isWeb} from 'platform/detection' -import {useModerationOpts} from '#/state/queries/preferences' -import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' -import {useProfileShadow} from '#/state/cache/profile-shadow' -import {useProfileFollowMutationQueue} from '#/state/queries/profile' -import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' +import {Button} from 'view/com/util/forms/Button' +import {Link} from 'view/com/util/Link' +import {Text} from 'view/com/util/text/Text' +import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' +import * as Toast from '../util/Toast' const OUTER_PADDING = 10 const INNER_PADDING = 14 @@ -218,8 +218,9 @@ function SuggestedFollow({ backgroundColor: pal.view.backgroundColor, }, ]}> - diff --git a/src/view/com/util/ErrorBoundary.tsx b/src/view/com/util/ErrorBoundary.tsx index 22fdd606e4..dccd2bbc9b 100644 --- a/src/view/com/util/ErrorBoundary.tsx +++ b/src/view/com/util/ErrorBoundary.tsx @@ -1,12 +1,14 @@ import React, {Component, ErrorInfo, ReactNode} from 'react' -import {ErrorScreen} from './error/ErrorScreen' -import {CenteredView} from './Views' import {msg} from '@lingui/macro' -import {logger} from '#/logger' import {useLingui} from '@lingui/react' +import {logger} from '#/logger' +import {ErrorScreen} from './error/ErrorScreen' +import {CenteredView} from './Views' + interface Props { children?: ReactNode + renderError?: (error: any) => ReactNode } interface State { @@ -30,6 +32,10 @@ export class ErrorBoundary extends Component { public render() { if (this.state.hasError) { + if (this.props.renderError) { + return this.props.renderError(this.state.error) + } + return ( diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index b6c512b09e..78d995ee82 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -2,34 +2,35 @@ import React, {ComponentProps, memo, useMemo} from 'react' import { GestureResponderEvent, Platform, + Pressable, StyleProp, - TextStyle, TextProps, + TextStyle, + TouchableOpacity, View, ViewStyle, - Pressable, - TouchableOpacity, } from 'react-native' -import {useLinkProps, StackActions} from '@react-navigation/native' -import {Text} from './text/Text' -import {TypographyVariant} from 'lib/ThemeContext' -import {router} from '../../../routes' -import { - convertBskyAppUrlIfNeeded, - isExternalUrl, - linkRequiresWarning, -} from 'lib/strings/url-helpers' -import {isAndroid, isWeb} from 'platform/detection' import {sanitizeUrl} from '@braintree/sanitize-url' -import {PressableWithHover} from './PressableWithHover' +import {StackActions, useLinkProps} from '@react-navigation/native' + import {useModalControls} from '#/state/modals' import {useOpenLink} from '#/state/preferences/in-app-browser' -import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper' import { DebouncedNavigationProp, useNavigationDeduped, } from 'lib/hooks/useNavigationDeduped' +import { + convertBskyAppUrlIfNeeded, + isExternalUrl, + linkRequiresWarning, +} from 'lib/strings/url-helpers' +import {TypographyVariant} from 'lib/ThemeContext' +import {isAndroid, isWeb} from 'platform/detection' +import {WebAuxClickWrapper} from 'view/com/util/WebAuxClickWrapper' import {useTheme} from '#/alf' +import {router} from '../../../routes' +import {PressableWithHover} from './PressableWithHover' +import {Text} from './text/Text' type Event = | React.MouseEvent @@ -147,8 +148,10 @@ export const TextLink = memo(function TextLink({ dataSet, title, onPress, + onBeforePress, disableMismatchWarning, navigationAction, + anchorNoUnderline, ...orgProps }: { testID?: string @@ -162,6 +165,8 @@ export const TextLink = memo(function TextLink({ title?: string disableMismatchWarning?: boolean navigationAction?: 'push' | 'replace' | 'navigate' + anchorNoUnderline?: boolean + onBeforePress?: () => void } & TextProps) { const {...props} = useLinkProps({to: sanitizeUrl(href)}) const navigation = useNavigationDeduped() @@ -172,6 +177,11 @@ export const TextLink = memo(function TextLink({ console.error('Unable to detect mismatching label') } + if (anchorNoUnderline) { + dataSet = dataSet ?? {} + dataSet.noUnderline = 1 + } + props.onPress = React.useCallback( (e?: Event) => { const requiresWarning = @@ -194,6 +204,7 @@ export const TextLink = memo(function TextLink({ // Let the browser handle opening in new tab etc. return } + onBeforePress?.() if (onPress) { e?.preventDefault?.() // @ts-ignore function signature differs by platform -prf @@ -218,6 +229,7 @@ export const TextLink = memo(function TextLink({ disableMismatchWarning, navigationAction, openLink, + onBeforePress, ], ) const hrefAttrs = useMemo(() => { @@ -266,7 +278,9 @@ interface TextLinkOnWebOnlyProps extends TextProps { title?: string navigationAction?: 'push' | 'replace' | 'navigate' disableMismatchWarning?: boolean + onBeforePress?: () => void onPointerEnter?: () => void + anchorNoUnderline?: boolean } export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ testID, @@ -278,6 +292,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ lineHeight, navigationAction, disableMismatchWarning, + onBeforePress, ...props }: TextLinkOnWebOnlyProps) { if (isWeb) { @@ -293,6 +308,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ title={props.title} navigationAction={navigationAction} disableMismatchWarning={disableMismatchWarning} + onBeforePress={onBeforePress} {...props} /> ) diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index d30a9d805b..d96a634ef2 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -1,11 +1,14 @@ import React, {memo} from 'react' import {FlatListProps, RefreshControl} from 'react-native' -import {FlatList_INTERNAL} from './Views' -import {addStyle} from 'lib/styles' -import {useScrollHandlers} from '#/lib/ScrollContext' import {runOnJS, useSharedValue} from 'react-native-reanimated' + import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {usePalette} from '#/lib/hooks/usePalette' +import {useScrollHandlers} from '#/lib/ScrollContext' +import {useGate} from 'lib/statsig/statsig' +import {addStyle} from 'lib/styles' +import {isWeb} from 'platform/detection' +import {FlatList_INTERNAL} from './Views' export type ListMethods = FlatList_INTERNAL export type ListProps = Omit< @@ -37,6 +40,7 @@ function ListImpl( const isScrolledDown = useSharedValue(false) const contextScrollHandlers = useScrollHandlers() const pal = usePalette('default') + const gate = useGate() function handleScrolledDownChange(didScrollDown: boolean) { onScrolledDownChange?.(didScrollDown) @@ -60,6 +64,11 @@ function ListImpl( } } }, + // Note: adding onMomentumBegin here makes simulator scroll + // lag on Android. So either don't add it, or figure out why. + onMomentumEnd(e, ctx) { + contextScrollHandlers.onMomentumEnd?.(e, ctx) + }, }) let refreshControl @@ -93,6 +102,9 @@ function ListImpl( scrollEventThrottle={1} style={style} ref={ref} + showsVerticalScrollIndicator={ + isWeb || !gate('hide_vertical_scroll_indicators') + } /> ) } diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx index 01b8a954d5..f45229dc42 100644 --- a/src/view/com/util/MainScrollProvider.tsx +++ b/src/view/com/util/MainScrollProvider.tsx @@ -1,11 +1,12 @@ import React, {useCallback, useEffect} from 'react' +import {NativeScrollEvent} from 'react-native' +import {interpolate, useSharedValue} from 'react-native-reanimated' import EventEmitter from 'eventemitter3' + import {ScrollProvider} from '#/lib/ScrollContext' -import {NativeScrollEvent} from 'react-native' -import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell' +import {useMinimalShellMode, useSetMinimalShellMode} from '#/state/shell' import {useShellLayout} from '#/state/shell/shell-layout' import {isNative, isWeb} from 'platform/detection' -import {useSharedValue, interpolate} from 'react-native-reanimated' const WEB_HIDE_SHELL_THRESHOLD = 200 @@ -32,6 +33,31 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { } }) + const snapToClosestState = useCallback( + (e: NativeScrollEvent) => { + 'worklet' + if (isNative) { + if (startDragOffset.value === null) { + return + } + const didScrollDown = e.contentOffset.y > startDragOffset.value + startDragOffset.value = null + startMode.value = null + if (e.contentOffset.y < headerHeight.value) { + // If we're close to the top, show the shell. + setMode(false) + } else if (didScrollDown) { + // Showing the bar again on scroll down feels annoying, so don't. + setMode(true) + } else { + // Snap to whichever state is the closest. + setMode(Math.round(mode.value) === 1) + } + } + }, + [startDragOffset, startMode, setMode, mode, headerHeight], + ) + const onBeginDrag = useCallback( (e: NativeScrollEvent) => { 'worklet' @@ -47,18 +73,24 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { (e: NativeScrollEvent) => { 'worklet' if (isNative) { - startDragOffset.value = null - startMode.value = null - if (e.contentOffset.y < headerHeight.value / 2) { - // If we're close to the top, show the shell. - setMode(false) - } else { - // Snap to whichever state is the closest. - setMode(Math.round(mode.value) === 1) + if (e.velocity && e.velocity.y !== 0) { + // If we detect a velocity, wait for onMomentumEnd to snap. + return } + snapToClosestState(e) } }, - [startDragOffset, startMode, setMode, mode, headerHeight], + [snapToClosestState], + ) + + const onMomentumEnd = useCallback( + (e: NativeScrollEvent) => { + 'worklet' + if (isNative) { + snapToClosestState(e) + } + }, + [snapToClosestState], ) const onScroll = useCallback( @@ -119,7 +151,8 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { + onScroll={onScroll} + onMomentumEnd={onMomentumEnd}> {children} ) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 529fc54e01..e7ce18535e 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -1,18 +1,21 @@ -import React, {memo} from 'react' +import React, {memo, useCallback} from 'react' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' -import {Text} from './text/Text' -import {TextLinkOnWebOnly} from './Link' -import {niceDate} from 'lib/strings/time' +import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' +import {useQueryClient} from '@tanstack/react-query' + +import {precacheProfile, usePrefetchProfileQuery} from '#/state/queries/profile' import {usePalette} from 'lib/hooks/usePalette' -import {TypographyVariant} from 'lib/ThemeContext' -import {UserAvatar} from './UserAvatar' +import {makeProfileLink} from 'lib/routes/links' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' +import {niceDate} from 'lib/strings/time' +import {TypographyVariant} from 'lib/ThemeContext' import {isAndroid, isWeb} from 'platform/detection' +import {ProfileHoverCard} from '#/components/ProfileHoverCard' +import {TextLinkOnWebOnly} from './Link' +import {Text} from './text/Text' import {TimeElapsed} from './TimeElapsed' -import {makeProfileLink} from 'lib/routes/links' -import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' -import {usePrefetchProfileQuery} from '#/state/queries/profile' +import {PreviewableUserAvatar} from './UserAvatar' interface PostMetaOpts { author: AppBskyActorDefs.ProfileViewBasic @@ -34,47 +37,61 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { const handle = opts.author.handle const prefetchProfileQuery = usePrefetchProfileQuery() + const profileLink = makeProfileLink(opts.author) + const onPointerEnter = isWeb + ? () => prefetchProfileQuery(opts.author.did) + : undefined + + const queryClient = useQueryClient() + const onBeforePress = useCallback(() => { + precacheProfile(queryClient, opts.author) + }, [queryClient, opts.author]) + return ( {opts.showAvatar && ( - )} - - + - {sanitizeDisplayName( - displayName, - opts.moderation?.ui('displayName'), - )} -   - - {sanitizeHandle(handle, '@')} - - - } - href={makeProfileLink(opts.author)} - onPointerEnter={ - isWeb ? () => prefetchProfileQuery(opts.author.did) : undefined - } - /> - + style={[styles.maxWidth, pal.textLight, opts.displayNameStyle]}> + + {sanitizeDisplayName( + displayName, + opts.moderation?.ui('displayName'), + )} + + } + href={profileLink} + onBeforePress={onBeforePress} + onPointerEnter={onPointerEnter} + /> + + + {!isAndroid && ( { title={niceDate(opts.timestamp)} accessibilityHint="" href={opts.postHref} + onBeforePress={onBeforePress} /> )} @@ -107,7 +125,7 @@ export {PostMeta} const styles = StyleSheet.create({ container: { flexDirection: 'row', - alignItems: 'center', + alignItems: 'flex-end', paddingBottom: 2, gap: 4, zIndex: 1, diff --git a/src/view/com/util/TimeElapsed.tsx b/src/view/com/util/TimeElapsed.tsx index aa3a092235..02b0f2314b 100644 --- a/src/view/com/util/TimeElapsed.tsx +++ b/src/view/com/util/TimeElapsed.tsx @@ -1,8 +1,7 @@ import React from 'react' -import {ago} from 'lib/strings/time' -import {useTickEveryMinute} from '#/state/shell' -// FIXME(dan): Figure out why the false positives +import {useTickEveryMinute} from '#/state/shell' +import {ago} from 'lib/strings/time' export function TimeElapsed({ timestamp, @@ -12,11 +11,13 @@ export function TimeElapsed({ children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element }) { const tick = useTickEveryMinute() - const [timeElapsed, setTimeAgo] = React.useState(ago(timestamp)) + const [timeElapsed, setTimeAgo] = React.useState(() => ago(timestamp)) - React.useEffect(() => { + const [prevTick, setPrevTick] = React.useState(tick) + if (prevTick !== tick) { + setPrevTick(tick) setTimeAgo(ago(timestamp)) - }, [timestamp, setTimeAgo, tick]) + } return children({timeElapsed}) } diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 4beedbd5b4..118e2ce2be 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -1,30 +1,34 @@ import React, {memo, useMemo} from 'react' import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' -import Svg, {Circle, Rect, Path} from 'react-native-svg' import {Image as RNImage} from 'react-native-image-crop-picker' -import {useLingui} from '@lingui/react' -import {msg, Trans} from '@lingui/macro' +import Svg, {Circle, Path, Rect} from 'react-native-svg' +import {AppBskyActorDefs, ModerationUI} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {ModerationUI} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' -import {HighPriorityImage} from 'view/com/util/images/Image' -import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' +import {usePalette} from 'lib/hooks/usePalette' import { - usePhotoLibraryPermission, useCameraPermission, + usePhotoLibraryPermission, } from 'lib/hooks/usePermissions' +import {makeProfileLink} from 'lib/routes/links' import {colors} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {isWeb, isAndroid, isNative} from 'platform/detection' -import {UserPreviewLink} from './UserPreviewLink' -import * as Menu from '#/components/Menu' +import {isAndroid, isNative, isWeb} from 'platform/detection' +import {precacheProfile} from 'state/queries/profile' +import {HighPriorityImage} from 'view/com/util/images/Image' +import {tokens, useTheme} from '#/alf' import { - Camera_Stroke2_Corner0_Rounded as Camera, Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, + Camera_Stroke2_Corner0_Rounded as Camera, } from '#/components/icons/Camera' import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive' import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' -import {useTheme, tokens} from '#/alf' +import {Link} from '#/components/Link' +import * as Menu from '#/components/Menu' +import {ProfileHoverCard} from '#/components/ProfileHoverCard' +import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler' @@ -45,8 +49,7 @@ interface EditableUserAvatarProps extends BaseUserAvatarProps { interface PreviewableUserAvatarProps extends BaseUserAvatarProps { moderation?: ModerationUI - did: string - handle: string + profile: AppBskyActorDefs.ProfileViewBasic } const BLUR_AMOUNT = isWeb ? 5 : 100 @@ -369,13 +372,30 @@ let EditableUserAvatar = ({ EditableUserAvatar = memo(EditableUserAvatar) export {EditableUserAvatar} -let PreviewableUserAvatar = ( - props: PreviewableUserAvatarProps, -): React.ReactNode => { +let PreviewableUserAvatar = ({ + moderation, + profile, + ...rest +}: PreviewableUserAvatarProps): React.ReactNode => { + const {_} = useLingui() + const queryClient = useQueryClient() + + const onPress = React.useCallback(() => { + precacheProfile(queryClient, profile) + }, [profile, queryClient]) + return ( - - - + + + + + ) } PreviewableUserAvatar = memo(PreviewableUserAvatar) diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx deleted file mode 100644 index a2c46afc01..0000000000 --- a/src/view/com/util/UserPreviewLink.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react' -import {StyleProp, ViewStyle} from 'react-native' -import {Link} from './Link' -import {isWeb} from 'platform/detection' -import {makeProfileLink} from 'lib/routes/links' -import {usePrefetchProfileQuery} from '#/state/queries/profile' - -interface UserPreviewLinkProps { - did: string - handle: string - style?: StyleProp -} -export function UserPreviewLink( - props: React.PropsWithChildren, -) { - const prefetchProfileQuery = usePrefetchProfileQuery() - return ( - { - if (isWeb) { - prefetchProfileQuery(props.did) - } - }} - href={makeProfileLink(props)} - title={props.handle} - asAnchor - style={props.style}> - {props.children} - - ) -} diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 872e10eef0..63a2b3de39 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -1,19 +1,20 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' +import Animated from 'react-native-reanimated' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {CenteredView} from './Views' -import {Text} from './text/Text' + +import {useSetDrawerOpen} from '#/state/shell' +import {useAnalytics} from 'lib/analytics/analytics' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {useAnalytics} from 'lib/analytics/analytics' import {NavigationProp} from 'lib/routes/types' -import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' -import Animated from 'react-native-reanimated' -import {useSetDrawerOpen} from '#/state/shell' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' import {useTheme} from '#/alf' +import {Text} from './text/Text' +import {CenteredView} from './Views' const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} @@ -62,6 +63,7 @@ export function ViewHeader({ return ( @@ -136,14 +138,17 @@ export function ViewHeader({ function DesktopWebHeader({ title, + subtitle, renderButton, showBorder = true, }: { title: string + subtitle?: string renderButton?: () => JSX.Element showBorder?: boolean }) { const pal = usePalette('default') + const t = useTheme() return ( - - - {title} - + + + + {title} + + + {renderButton?.()} - {renderButton?.()} + {subtitle ? ( + + + + {subtitle} + + + + ) : null} ) } @@ -236,6 +258,9 @@ const styles = StyleSheet.create({ subtitle: { fontSize: 13, }, + subtitleDesktop: { + fontSize: 15, + }, backBtn: { width: 30, height: 30, diff --git a/src/view/com/util/Views.jsx b/src/view/com/util/Views.jsx index 7d6120583f..75f2b50814 100644 --- a/src/view/com/util/Views.jsx +++ b/src/view/com/util/Views.jsx @@ -2,8 +2,19 @@ import React from 'react' import {View} from 'react-native' import Animated from 'react-native-reanimated' +import {useGate} from 'lib/statsig/statsig' + export const FlatList_INTERNAL = Animated.FlatList -export const ScrollView = Animated.ScrollView export function CenteredView(props) { return } + +export function ScrollView(props) { + const gate = useGate() + return ( + + ) +} diff --git a/src/view/com/util/forms/NativeDropdown.web.tsx b/src/view/com/util/forms/NativeDropdown.web.tsx index 94591d3931..6668ac211f 100644 --- a/src/view/com/util/forms/NativeDropdown.web.tsx +++ b/src/view/com/util/forms/NativeDropdown.web.tsx @@ -1,12 +1,13 @@ import React from 'react' +import {Pressable, StyleSheet, Text, View, ViewStyle} from 'react-native' +import {IconProp} from '@fortawesome/fontawesome-svg-core' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import {Pressable, StyleSheet, View, Text, ViewStyle} from 'react-native' -import {IconProp} from '@fortawesome/fontawesome-svg-core' import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' + +import {HITSLOP_10} from 'lib/constants' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' -import {HITSLOP_10} from 'lib/constants' // Custom Dropdown Menu Components // == @@ -64,15 +65,9 @@ export function NativeDropdown({ accessibilityHint, triggerStyle, }: React.PropsWithChildren) { - const pal = usePalette('default') - const theme = useTheme() - const dropDownBackgroundColor = - theme.colorScheme === 'dark' ? pal.btn : pal.view const [open, setOpen] = React.useState(false) const buttonRef = React.useRef(null) const menuRef = React.useRef(null) - const {borderColor: separatorColor} = - theme.colorScheme === 'dark' ? pal.borderDark : pal.border React.useEffect(() => { function clickHandler(e: MouseEvent) { @@ -114,14 +109,27 @@ export function NativeDropdown({ return ( setOpen(o)}> - e.preventDefault()}> + } testID={testID} accessibilityRole="button" accessibilityLabel={accessibilityLabel} accessibilityHint={accessibilityHint} - onPress={() => setOpen(o => !o)} + onPointerDown={e => { + // Prevent false positive that interpret mobile scroll as a tap. + // This requires the custom onPress handler below to compensate. + // https://github.com/radix-ui/primitives/issues/1912 + e.preventDefault() + }} + onPress={() => { + if (window.event instanceof KeyboardEvent) { + // The onPointerDown hack above is not relevant to this press, so don't do anything. + return + } + // Compensate for the disabled onPointerDown above by triggering it manually. + setOpen(o => !o) + }} hitSlop={HITSLOP_10} style={triggerStyle}> {children} @@ -129,53 +137,53 @@ export function NativeDropdown({ - - {items.map((item, index) => { - if (item.label === 'separator') { - return ( - - ) - } - if (index > 1 && items[index - 1].label === 'separator') { - return ( - - - - {item.label} - - {item.icon && ( - - )} - - - ) - } - return ( + + + + ) +} + +function DropdownContent({ + items, + menuRef, +}: { + items: DropdownItem[] + menuRef: React.RefObject +}) { + const pal = usePalette('default') + const theme = useTheme() + const dropDownBackgroundColor = + theme.colorScheme === 'dark' ? pal.btn : pal.view + const {borderColor: separatorColor} = + theme.colorScheme === 'dark' ? pal.borderDark : pal.border + + return ( + + {items.map((item, index) => { + if (item.label === 'separator') { + return ( + + ) + } + if (index > 1 && items[index - 1].label === 'separator') { + return ( + @@ -190,11 +198,27 @@ export function NativeDropdown({ /> )} - ) - })} - - - + + ) + } + return ( + + + {item.label} + + {item.icon && ( + + )} + + ) + })} + ) } diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 959e0f692e..32520182e9 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -28,12 +28,14 @@ import {getCurrentRoute} from 'lib/routes/helpers' import {shareUrl} from 'lib/sharing' import {toShareUrl} from 'lib/strings/url-helpers' import {useTheme} from 'lib/ThemeContext' -import {atoms as a, useTheme as useAlf} from '#/alf' +import {atoms as a, useBreakpoints, useTheme as useAlf} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {EmbedDialog} from '#/components/dialogs/Embed' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' +import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets' import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' @@ -55,6 +57,7 @@ let PostDropdownBtn = ({ richText, style, hitSlop, + timestamp, }: { testID: string postAuthor: AppBskyActorDefs.ProfileViewBasic @@ -64,10 +67,12 @@ let PostDropdownBtn = ({ richText: RichTextAPI style?: StyleProp hitSlop?: PressableProps['hitSlop'] + timestamp: string }): React.ReactNode => { const {hasSession, currentAccount} = useSession() const theme = useTheme() const alf = useAlf() + const {gtMobile} = useBreakpoints() const {_} = useLingui() const defaultCtrlColor = theme.palette.default.postCtrl const langPrefs = useLanguagePrefs() @@ -83,6 +88,7 @@ let PostDropdownBtn = ({ const deletePromptControl = useDialogControl() const hidePromptControl = useDialogControl() const loggedOutWarningPromptControl = useDialogControl() + const embedPostControl = useDialogControl() const rootUri = record.reply?.root?.uri || postUri const isThreadMuted = mutedThreads.includes(rootUri) @@ -166,7 +172,7 @@ let PostDropdownBtn = ({ hidePost({uri: postUri}) }, [postUri, hidePost]) - const shouldShowLoggedOutWarning = React.useMemo(() => { + const hideInPWI = React.useMemo(() => { return !!postAuthor.labels?.find( label => label.val === '!no-unauthenticated', ) @@ -177,6 +183,8 @@ let PostDropdownBtn = ({ shareUrl(url) }, [href]) + const canEmbed = isWeb && gtMobile && !hideInPWI + return ( @@ -207,27 +215,31 @@ let PostDropdownBtn = ({ - - {_(msg`Translate`)} - - + {(!hideInPWI || hasSession) && ( + <> + + {_(msg`Translate`)} + + - - {_(msg`Copy post text`)} - - + + {_(msg`Copy post text`)} + + + + )} { - if (shouldShowLoggedOutWarning) { + if (hideInPWI) { loggedOutWarningPromptControl.open() } else { onSharePost() @@ -238,6 +250,16 @@ let PostDropdownBtn = ({ + + {canEmbed && ( + + {_(msg`Embed post`)} + + + )} {hasSession && ( @@ -283,29 +305,33 @@ let PostDropdownBtn = ({ )} - + {hasSession && ( + <> + - - {!isAuthor && ( - reportDialogControl.open()}> - {_(msg`Report post`)} - - - )} + + {!isAuthor && ( + reportDialogControl.open()}> + {_(msg`Report post`)} + + + )} - {isAuthor && ( - - {_(msg`Delete post`)} - - - )} - + {isAuthor && ( + + {_(msg`Delete post`)} + + + )} + + + )} @@ -346,6 +372,17 @@ let PostDropdownBtn = ({ onConfirm={onSharePost} confirmButtonCta={_(msg`Share anyway`)} /> + + {canEmbed && ( + + )} ) } diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 58874cd551..cb50ee6dc3 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -16,7 +16,6 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {HITSLOP_10, HITSLOP_20} from '#/lib/constants' -import {Haptics} from '#/lib/haptics' import {CommentBottomArrow, HeartIcon, HeartIconSolid} from '#/lib/icons' import {makeProfileLink} from '#/lib/routes/links' import {shareUrl} from '#/lib/sharing' @@ -32,6 +31,7 @@ import { } from '#/state/queries/post' import {useRequireAuth} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' +import {useHaptics} from 'lib/haptics' import {useDialogControl} from '#/components/Dialog' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox' import * as Prompt from '#/components/Prompt' @@ -67,6 +67,7 @@ let PostCtrls = ({ ) const requireAuth = useRequireAuth() const loggedOutWarningPromptControl = useDialogControl() + const playHaptic = useHaptics() const shouldShowLoggedOutWarning = React.useMemo(() => { return !!post.author.labels?.find( @@ -84,7 +85,7 @@ let PostCtrls = ({ const onPressToggleLike = React.useCallback(async () => { try { if (!post.viewer?.like) { - Haptics.default() + playHaptic() await queueLike() } else { await queueUnlike() @@ -94,13 +95,13 @@ let PostCtrls = ({ throw e } } - }, [post.viewer?.like, queueLike, queueUnlike]) + }, [playHaptic, post.viewer?.like, queueLike, queueUnlike]) const onRepost = useCallback(async () => { closeModal() try { if (!post.viewer?.repost) { - Haptics.default() + playHaptic() await queueRepost() } else { await queueUnrepost() @@ -110,7 +111,7 @@ let PostCtrls = ({ throw e } } - }, [post.viewer?.repost, queueRepost, queueUnrepost, closeModal]) + }, [closeModal, post.viewer?.repost, playHaptic, queueRepost, queueUnrepost]) const onQuote = useCallback(() => { closeModal() @@ -123,15 +124,16 @@ let PostCtrls = ({ indexedAt: post.indexedAt, }, }) - Haptics.default() + playHaptic() }, [ + closeModal, + openComposer, post.uri, post.cid, post.author, post.indexedAt, record.text, - openComposer, - closeModal, + playHaptic, ]) const onShare = useCallback(() => { @@ -262,6 +264,7 @@ let PostCtrls = ({ richText={richText} style={styles.btnPad} hitSlop={big ? HITSLOP_20 : HITSLOP_10} + timestamp={post.indexedAt} /> diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx index aaa98a41f6..1fe75c44ea 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -1,20 +1,28 @@ -import React from 'react' +import React, {useCallback} from 'react' +import {StyleProp, View, ViewStyle} from 'react-native' import {Image} from 'expo-image' -import {Text} from '../text/Text' -import {StyleSheet, View} from 'react-native' +import {AppBskyEmbedExternal} from '@atproto/api' + import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {AppBskyEmbedExternal} from '@atproto/api' -import {toNiceDomain} from 'lib/strings/url-helpers' +import {shareUrl} from 'lib/sharing' import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' -import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed' -import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed' +import {toNiceDomain} from 'lib/strings/url-helpers' +import {isNative} from 'platform/detection' import {useExternalEmbedsPrefs} from 'state/preferences' +import {Link} from 'view/com/util/Link' +import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed' +import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed' +import {GifEmbed} from 'view/com/util/post-embeds/GifEmbed' +import {atoms as a, useTheme} from '#/alf' +import {Text} from '../text/Text' export const ExternalLinkEmbed = ({ link, + style, }: { link: AppBskyEmbedExternal.ViewExternal + style?: StyleProp }) => { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() @@ -28,62 +36,95 @@ export const ExternalLinkEmbed = ({ } }, [link.uri, externalEmbedPrefs]) + if (embedPlayerParams?.source === 'tenor') { + return + } + return ( - - {link.thumb && !embedPlayerParams ? ( - - ) : undefined} - {(embedPlayerParams?.isGif && ( - - )) || - (embedPlayerParams && ( + + + {link.thumb && !embedPlayerParams ? ( + + ) : undefined} + {embedPlayerParams?.isGif ? ( + + ) : embedPlayerParams ? ( - ))} - - - {toNiceDomain(link.uri)} - - {!embedPlayerParams?.isGif && ( - - {link.title || link.uri} - - )} - {link.description && !embedPlayerParams?.hideDetails ? ( + ) : undefined} + - {link.description} + type="sm" + numberOfLines={1} + style={[pal.textLight, {marginVertical: 2}]}> + {toNiceDomain(link.uri)} - ) : undefined} - + + {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && ( + + {link.title || link.uri} + + )} + {link.description ? ( + + {link.description} + + ) : undefined} + + ) } -const styles = StyleSheet.create({ - container: { - flexDirection: 'column', - borderRadius: 6, - overflow: 'hidden', - }, - info: { - width: '100%', - bottom: 0, - paddingTop: 8, - paddingBottom: 10, - }, - extUri: { - marginTop: 2, - }, - extDescription: { - marginTop: 4, - }, -}) +function LinkWrapper({ + link, + style, + children, +}: { + link: AppBskyEmbedExternal.ViewExternal + style?: StyleProp + children: React.ReactNode +}) { + const t = useTheme() + + const onShareExternal = useCallback(() => { + if (link.uri && isNative) { + shareUrl(link.uri) + } + }, [link.uri]) + + return ( + + {children} + + ) +} diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/view/com/util/post-embeds/GifEmbed.tsx new file mode 100644 index 0000000000..5d21ce0642 --- /dev/null +++ b/src/view/com/util/post-embeds/GifEmbed.tsx @@ -0,0 +1,141 @@ +import React from 'react' +import {Pressable, View} from 'react-native' +import {AppBskyEmbedExternal} from '@atproto/api' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {EmbedPlayerParams} from 'lib/strings/embed-player' +import {useAutoplayDisabled} from 'state/preferences' +import {atoms as a, useTheme} from '#/alf' +import {Loader} from '#/components/Loader' +import {GifView} from '../../../../../modules/expo-bluesky-gif-view' +import {GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types' + +function PlaybackControls({ + onPress, + isPlaying, + isLoaded, +}: { + onPress: () => void + isPlaying: boolean + isLoaded: boolean +}) { + const {_} = useLingui() + const t = useTheme() + + return ( + + {!isLoaded ? ( + + + + + + ) : !isPlaying ? ( + + + + ) : undefined} + + ) +} + +export function GifEmbed({ + params, + link, +}: { + params: EmbedPlayerParams + link: AppBskyEmbedExternal.ViewExternal +}) { + const {_} = useLingui() + const autoplayDisabled = useAutoplayDisabled() + + const playerRef = React.useRef(null) + + const [playerState, setPlayerState] = React.useState<{ + isPlaying: boolean + isLoaded: boolean + }>({ + isPlaying: !autoplayDisabled, + isLoaded: false, + }) + + const onPlayerStateChange = React.useCallback( + (e: GifViewStateChangeEvent) => { + setPlayerState(e.nativeEvent) + }, + [], + ) + + const onPress = React.useCallback(() => { + playerRef.current?.toggleAsync() + }, []) + + return ( + + + + + + + ) +} diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 2b1c3e6179..e0178f34b4 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -1,31 +1,44 @@ import React from 'react' -import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import { - AppBskyFeedDefs, - AppBskyEmbedRecord, - AppBskyFeedPost, + StyleProp, + StyleSheet, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native' +import { + AppBskyEmbedExternal, AppBskyEmbedImages, + AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, - AppBskyEmbedExternal, - RichText as RichTextAPI, + AppBskyFeedDefs, + AppBskyFeedPost, moderatePost, ModerationDecision, + RichText as RichTextAPI, } from '@atproto/api' import {AtUri} from '@atproto/api' -import {PostMeta} from '../PostMeta' -import {Link} from '../Link' -import {Text} from '../text/Text' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' + +import {HITSLOP_20} from '#/lib/constants' +import {s} from '#/lib/styles' +import {useModerationOpts} from '#/state/queries/preferences' import {usePalette} from 'lib/hooks/usePalette' -import {ComposerOptsQuote} from 'state/shell/composer' -import {PostEmbeds} from '.' -import {PostAlerts} from '../../../../components/moderation/PostAlerts' -import {makeProfileLink} from 'lib/routes/links' import {InfoCircleIcon} from 'lib/icons' -import {Trans} from '@lingui/macro' -import {useModerationOpts} from '#/state/queries/preferences' -import {ContentHider} from '../../../../components/moderation/ContentHider' -import {RichText} from '#/components/RichText' +import {makeProfileLink} from 'lib/routes/links' +import {precacheProfile} from 'state/queries/profile' +import {ComposerOptsQuote} from 'state/shell/composer' import {atoms as a} from '#/alf' +import {RichText} from '#/components/RichText' +import {ContentHider} from '../../../../components/moderation/ContentHider' +import {PostAlerts} from '../../../../components/moderation/PostAlerts' +import {Link} from '../Link' +import {PostMeta} from '../PostMeta' +import {Text} from '../text/Text' +import {PostEmbeds} from '.' export function MaybeQuoteEmbed({ embed, @@ -107,6 +120,7 @@ export function QuoteEmbed({ moderation?: ModerationDecision style?: StyleProp }) { + const queryClient = useQueryClient() const pal = usePalette('default') const itemUrip = new AtUri(quote.uri) const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) @@ -134,13 +148,18 @@ export function QuoteEmbed({ } }, [quote.embeds]) + const onBeforePress = React.useCallback(() => { + precacheProfile(queryClient, quote.author) + }, [queryClient, quote.author]) + return ( + title={itemTitle} + onBeforePress={onBeforePress}> void}) { + const {_} = useLingui() + return ( + + + + ) +} + function viewRecordToPostView( viewRecord: AppBskyEmbedRecord.ViewRecord, ): AppBskyFeedDefs.PostView { diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 47091fbb07..7ea5b55cfe 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -1,34 +1,32 @@ -import React, {useCallback} from 'react' +import React from 'react' import { - StyleSheet, + InteractionManager, StyleProp, + StyleSheet, + Text, View, ViewStyle, - Text, - InteractionManager, } from 'react-native' import {Image} from 'expo-image' import { - AppBskyEmbedImages, AppBskyEmbedExternal, + AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyFeedDefs, AppBskyGraphDefs, ModerationDecision, } from '@atproto/api' -import {Link} from '../Link' -import {ImageLayoutGrid} from '../images/ImageLayoutGrid' -import {useLightboxControls, ImagesLightbox} from '#/state/lightbox' + +import {ImagesLightbox, useLightboxControls} from '#/state/lightbox' import {usePalette} from 'lib/hooks/usePalette' -import {ExternalLinkEmbed} from './ExternalLinkEmbed' -import {MaybeQuoteEmbed} from './QuoteEmbed' -import {AutoSizedImage} from '../images/AutoSizedImage' -import {ListEmbed} from './ListEmbed' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {ContentHider} from '../../../../components/moderation/ContentHider' -import {isNative} from '#/platform/detection' -import {shareUrl} from '#/lib/sharing' +import {AutoSizedImage} from '../images/AutoSizedImage' +import {ImageLayoutGrid} from '../images/ImageLayoutGrid' +import {ExternalLinkEmbed} from './ExternalLinkEmbed' +import {ListEmbed} from './ListEmbed' +import {MaybeQuoteEmbed} from './QuoteEmbed' type Embed = | AppBskyEmbedRecord.View @@ -49,16 +47,6 @@ export function PostEmbeds({ const pal = usePalette('default') const {openLightbox} = useLightboxControls() - const externalUri = AppBskyEmbedExternal.isView(embed) - ? embed.external.uri - : null - - const onShareExternal = useCallback(() => { - if (externalUri && isNative) { - shareUrl(externalUri) - } - }, [externalUri]) - // quote post with media // = if (AppBskyEmbedRecordWithMedia.isView(embed)) { @@ -161,18 +149,9 @@ export function PostEmbeds({ // = if (AppBskyEmbedExternal.isView(embed)) { const link = embed.external - return ( - - - + ) } @@ -187,11 +166,6 @@ const styles = StyleSheet.create({ singleImage: { borderRadius: 8, }, - extOuter: { - borderWidth: 1, - borderRadius: 8, - marginTop: 4, - }, altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', borderRadius: 6, diff --git a/src/view/icons/index.tsx b/src/view/icons/index.tsx index ede1e63355..b9af6a519c 100644 --- a/src/view/icons/index.tsx +++ b/src/view/icons/index.tsx @@ -1,63 +1,72 @@ import {library} from '@fortawesome/fontawesome-svg-core' - import {faAddressCard} from '@fortawesome/free-regular-svg-icons' +import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell' +import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark' +import {faCalendar as farCalendar} from '@fortawesome/free-regular-svg-icons/faCalendar' +import {faCircle} from '@fortawesome/free-regular-svg-icons/faCircle' +import {faCircleCheck as farCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck' +import {faCirclePlay} from '@fortawesome/free-regular-svg-icons/faCirclePlay' +import {faCircleUser} from '@fortawesome/free-regular-svg-icons/faCircleUser' +import {faClone as farClone} from '@fortawesome/free-regular-svg-icons/faClone' +import {faComment} from '@fortawesome/free-regular-svg-icons/faComment' +import {faComments} from '@fortawesome/free-regular-svg-icons/faComments' +import {faCompass} from '@fortawesome/free-regular-svg-icons/faCompass' +import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash' +import {faFaceSmile} from '@fortawesome/free-regular-svg-icons/faFaceSmile' +import {faFloppyDisk} from '@fortawesome/free-regular-svg-icons/faFloppyDisk' +import {faHand as farHand} from '@fortawesome/free-regular-svg-icons/faHand' +import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart' +import {faImage as farImage} from '@fortawesome/free-regular-svg-icons/faImage' +import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage' +import {faPaste} from '@fortawesome/free-regular-svg-icons/faPaste' +import {faSquare} from '@fortawesome/free-regular-svg-icons/faSquare' +import {faSquareCheck} from '@fortawesome/free-regular-svg-icons/faSquareCheck' +import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus' +import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' +import {faUser} from '@fortawesome/free-regular-svg-icons/faUser' +import {faFlask} from '@fortawesome/free-solid-svg-icons' +import {faUniversalAccess} from '@fortawesome/free-solid-svg-icons' import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown' import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft' import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight' import {faAngleUp} from '@fortawesome/free-solid-svg-icons/faAngleUp' +import {faArrowDown} from '@fortawesome/free-solid-svg-icons/faArrowDown' import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft' import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight' -import {faArrowUp} from '@fortawesome/free-solid-svg-icons/faArrowUp' -import {faArrowDown} from '@fortawesome/free-solid-svg-icons/faArrowDown' import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowRightFromBracket' -import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket' -import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare' import {faArrowRotateLeft} from '@fortawesome/free-solid-svg-icons/faArrowRotateLeft' -import {faArrowTrendUp} from '@fortawesome/free-solid-svg-icons/faArrowTrendUp' import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate' +import {faArrowTrendUp} from '@fortawesome/free-solid-svg-icons/faArrowTrendUp' +import {faArrowUp} from '@fortawesome/free-solid-svg-icons/faArrowUp' +import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket' +import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare' import {faAt} from '@fortawesome/free-solid-svg-icons/faAt' -import {faBars} from '@fortawesome/free-solid-svg-icons/faBars' import {faBan} from '@fortawesome/free-solid-svg-icons/faBan' +import {faBars} from '@fortawesome/free-solid-svg-icons/faBars' import {faBell} from '@fortawesome/free-solid-svg-icons/faBell' -import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell' import {faBookmark} from '@fortawesome/free-solid-svg-icons/faBookmark' -import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark' -import {faCalendar as farCalendar} from '@fortawesome/free-regular-svg-icons/faCalendar' import {faCamera} from '@fortawesome/free-solid-svg-icons/faCamera' import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck' +import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown' import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight' -import {faCircle} from '@fortawesome/free-regular-svg-icons/faCircle' -import {faCircleCheck as farCircleCheck} from '@fortawesome/free-regular-svg-icons/faCircleCheck' import {faCircleCheck} from '@fortawesome/free-solid-svg-icons/faCircleCheck' import {faCircleDot} from '@fortawesome/free-solid-svg-icons/faCircleDot' import {faCircleExclamation} from '@fortawesome/free-solid-svg-icons/faCircleExclamation' -import {faCirclePlay} from '@fortawesome/free-regular-svg-icons/faCirclePlay' -import {faCircleUser} from '@fortawesome/free-regular-svg-icons/faCircleUser' import {faClone} from '@fortawesome/free-solid-svg-icons/faClone' -import {faClone as farClone} from '@fortawesome/free-regular-svg-icons/faClone' -import {faComment} from '@fortawesome/free-regular-svg-icons/faComment' import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash' -import {faComments} from '@fortawesome/free-regular-svg-icons/faComments' -import {faCompass} from '@fortawesome/free-regular-svg-icons/faCompass' import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload' import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis' import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope' import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation' import {faEye} from '@fortawesome/free-solid-svg-icons/faEye' -import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash' -import {faFaceSmile} from '@fortawesome/free-regular-svg-icons/faFaceSmile' +import {faFilter} from '@fortawesome/free-solid-svg-icons/faFilter' import {faFire} from '@fortawesome/free-solid-svg-icons/faFire' -import {faFlask} from '@fortawesome/free-solid-svg-icons' -import {faFloppyDisk} from '@fortawesome/free-regular-svg-icons/faFloppyDisk' import {faGear} from '@fortawesome/free-solid-svg-icons/faGear' import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe' import {faHand} from '@fortawesome/free-solid-svg-icons/faHand' -import {faHand as farHand} from '@fortawesome/free-regular-svg-icons/faHand' import {faHashtag} from '@fortawesome/free-solid-svg-icons/faHashtag' -import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart' import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart' import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse' -import {faImage as farImage} from '@fortawesome/free-regular-svg-icons/faImage' import {faImage} from '@fortawesome/free-solid-svg-icons/faImage' import {faInfo} from '@fortawesome/free-solid-svg-icons/faInfo' import {faLanguage} from '@fortawesome/free-solid-svg-icons/faLanguage' @@ -66,10 +75,8 @@ import {faList} from '@fortawesome/free-solid-svg-icons/faList' import {faListUl} from '@fortawesome/free-solid-svg-icons/faListUl' import {faLock} from '@fortawesome/free-solid-svg-icons/faLock' import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass' -import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage' import {faNoteSticky} from '@fortawesome/free-solid-svg-icons/faNoteSticky' import {faPause} from '@fortawesome/free-solid-svg-icons/faPause' -import {faPaste} from '@fortawesome/free-regular-svg-icons/faPaste' import {faPen} from '@fortawesome/free-solid-svg-icons/faPen' import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib' import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare' @@ -87,23 +94,16 @@ import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSq import {faShield} from '@fortawesome/free-solid-svg-icons/faShield' import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal' import {faSliders} from '@fortawesome/free-solid-svg-icons/faSliders' -import {faSquare} from '@fortawesome/free-regular-svg-icons/faSquare' -import {faSquareCheck} from '@fortawesome/free-regular-svg-icons/faSquareCheck' -import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus' import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack' import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' -import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' -import {faUser} from '@fortawesome/free-regular-svg-icons/faUser' -import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers' import {faUserCheck} from '@fortawesome/free-solid-svg-icons/faUserCheck' -import {faUserSlash} from '@fortawesome/free-solid-svg-icons/faUserSlash' import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus' -import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark' +import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers' +import {faUserSlash} from '@fortawesome/free-solid-svg-icons/faUserSlash' import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash' +import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark' import {faX} from '@fortawesome/free-solid-svg-icons/faX' import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' -import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown' -import {faFilter} from '@fortawesome/free-solid-svg-icons/faFilter' library.add( faAddressCard, @@ -196,6 +196,7 @@ library.add( faSquare, faSquareCheck, faSquarePlus, + faUniversalAccess, faUser, faUsers, faUserCheck, diff --git a/src/view/screens/AccessibilitySettings.tsx b/src/view/screens/AccessibilitySettings.tsx new file mode 100644 index 0000000000..ac0d985f10 --- /dev/null +++ b/src/view/screens/AccessibilitySettings.tsx @@ -0,0 +1,132 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' + +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, + useRequireAltTextEnabled, + 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' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'AccessibilitySettings' +> +export function AccessibilitySettingsScreen({}: Props) { + const pal = usePalette('default') + const setMinimalShellMode = useSetMinimalShellMode() + const {screen} = useAnalytics() + const {isMobile} = useWebMediaQueries() + const {_} = useLingui() + + const requireAltTextEnabled = useRequireAltTextEnabled() + const setRequireAltTextEnabled = useSetRequireAltTextEnabled() + const autoplayDisabled = useAutoplayDisabled() + const setAutoplayDisabled = useSetAutoplayDisabled() + const hapticsDisabled = useHapticsDisabled() + const setHapticsDisabled = useSetHapticsDisabled() + + useFocusEffect( + React.useCallback(() => { + screen('PreferencesExternalEmbeds') + setMinimalShellMode(false) + }, [screen, setMinimalShellMode]), + ) + + return ( + + + + + Accessibility Settings + + + + + + Alt text + + + setRequireAltTextEnabled(!requireAltTextEnabled)} + /> + + + Media + + + setAutoplayDisabled(!autoplayDisabled)} + /> + + {isNative && ( + <> + + Haptics + + + setHapticsDisabled(!hapticsDisabled)} + /> + + + )} + + + ) +} + +const styles = StyleSheet.create({ + heading: { + paddingHorizontal: 18, + paddingTop: 14, + paddingBottom: 6, + }, + toggleCard: { + paddingVertical: 8, + paddingHorizontal: 6, + marginBottom: 1, + }, +}) diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 2e3bf08db5..e64ab08df2 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -1,52 +1,53 @@ import React from 'react' import { ActivityIndicator, - StyleSheet, - View, type FlatList, Pressable, + StyleSheet, + View, } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' -import {ViewHeader} from 'view/com/util/ViewHeader' -import {FAB} from 'view/com/util/fab/FAB' -import {Link} from 'view/com/util/Link' -import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {ComposeIcon2, CogIcon, MagnifyingGlassIcon2} from 'lib/icons' -import {s} from 'lib/styles' -import {atoms as a, useTheme} from '#/alf' -import {SearchInput, SearchInputRef} from 'view/com/util/forms/SearchInput' -import {UserAvatar} from 'view/com/util/UserAvatar' -import { - LoadingPlaceholder, - FeedFeedLoadingPlaceholder, -} from 'view/com/util/LoadingPlaceholder' -import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import debounce from 'lodash.debounce' -import {Text} from 'view/com/util/text/Text' -import {List} from 'view/com/util/List' -import {useFocusEffect} from '@react-navigation/native' -import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' -import {Trans, msg} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useSetMinimalShellMode} from '#/state/shell' -import {usePreferencesQuery} from '#/state/queries/preferences' +import {useFocusEffect} from '@react-navigation/native' +import debounce from 'lodash.debounce' + +import {isNative, isWeb} from '#/platform/detection' import { + getAvatarTypeFromUri, useFeedSourceInfoQuery, useGetPopularFeedsQuery, useSearchPopularFeedsMutation, - getAvatarTypeFromUri, } from '#/state/queries/feed' -import {cleanError} from 'lib/strings/errors' -import {useComposerControls} from '#/state/shell/composer' +import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' -import {isNative, isWeb} from '#/platform/detection' +import {useSetMinimalShellMode} from '#/state/shell' +import {useComposerControls} from '#/state/shell/composer' import {HITSLOP_10} from 'lib/constants' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {CogIcon, ComposeIcon2, MagnifyingGlassIcon2} from 'lib/icons' +import {FeedsTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' +import {cleanError} from 'lib/strings/errors' +import {s} from 'lib/styles' +import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' +import {ErrorMessage} from 'view/com/util/error/ErrorMessage' +import {FAB} from 'view/com/util/fab/FAB' +import {SearchInput, SearchInputRef} from 'view/com/util/forms/SearchInput' +import {Link} from 'view/com/util/Link' +import {List} from 'view/com/util/List' +import { + FeedFeedLoadingPlaceholder, + LoadingPlaceholder, +} from 'view/com/util/LoadingPlaceholder' +import {Text} from 'view/com/util/text/Text' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {atoms as a, useTheme} from '#/alf' import {IconCircle} from '#/components/IconCircle' -import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass' +import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' type Props = NativeStackScreenProps @@ -100,6 +101,22 @@ type FlatlistSlice = key: string } +// HACK +// the protocol doesn't yet tell us which feeds are personalized +// this list is used to filter out feed recommendations from logged out users +// for the ones we know need it +// -prf +const KNOWN_AUTHED_ONLY_FEEDS = [ + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends', // popular with friends, by bsky.app + 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/mutuals', // mutuals, by skyfeed + 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/only-posts', // only posts, by skyfeed + 'at://did:plc:wzsilnxf24ehtmmc3gssy5bu/app.bsky.feed.generator/mentions', // mentions, by flicknow + 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/bangers', // my bangers, by jaz + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/mutuals', // mutuals, by bluesky + 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/my-followers', // followers, by jaz + 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why +] + export function FeedsScreen(_props: Props) { const pal = usePalette('default') const {openComposer} = useComposerControls() @@ -299,7 +316,15 @@ export function FeedsScreen(_props: Props) { for (const page of popularFeeds.pages || []) { slices = slices.concat( page.feeds - .filter(feed => !preferences?.feeds?.saved.includes(feed.uri)) + .filter(feed => { + if ( + !hasSession && + KNOWN_AUTHED_ONLY_FEEDS.includes(feed.uri) + ) { + return false + } + return !preferences?.feeds?.saved.includes(feed.uri) + }) .map(feed => ({ key: `popularFeed:${feed.uri}`, type: 'popularFeed', diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 39bdac669c..3eaa1b8757 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -2,8 +2,10 @@ import React from 'react' import {ActivityIndicator, AppState, StyleSheet, View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' +import {PROD_DEFAULT_FEED} from '#/lib/constants' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useSetTitle} from '#/lib/hooks/useSetTitle' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {logEvent, LogEvents, useGate} from '#/lib/statsig/statsig' import {emitSoftReset} from '#/state/events' import {FeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed' @@ -11,15 +13,19 @@ import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {usePreferencesQuery} from '#/state/queries/preferences' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {useSession} from '#/state/session' -import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' +import { + useMinimalShellMode, + useSetDrawerSwipeDisabled, + useSetMinimalShellMode, +} from '#/state/shell' import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' +import {useOTAUpdates} from 'lib/hooks/useOTAUpdates' import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import {FeedPage} from 'view/com/feeds/FeedPage' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' -import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA' import {HomeHeader} from '../com/home/HomeHeader' type Props = NativeStackScreenProps @@ -51,6 +57,8 @@ function HomeScreenReady({ preferences: UsePreferencesQueryResponse pinnedFeedInfos: FeedSourceInfo[] }) { + useOTAUpdates() + const allFeeds = React.useMemo(() => { const feeds: FeedDescriptor[] = [] feeds.push('home') @@ -108,21 +116,25 @@ function HomeScreenReady({ }), ) - const disableMinShellOnForegrounding = useGate( - 'disable_min_shell_on_foregrounding', - ) + const gate = useGate() + const mode = useMinimalShellMode() + const {isMobile} = useWebMediaQueries() React.useEffect(() => { - if (disableMinShellOnForegrounding) { - const listener = AppState.addEventListener('change', nextAppState => { - if (nextAppState === 'active') { + const listener = AppState.addEventListener('change', nextAppState => { + if (nextAppState === 'active') { + if ( + isMobile && + mode.value === 1 && + gate('disable_min_shell_on_foregrounding_v2') + ) { setMinimalShellMode(false) } - }) - return () => { - listener.remove() } + }) + return () => { + listener.remove() } - }, [setMinimalShellMode, disableMinShellOnForegrounding]) + }, [setMinimalShellMode, mode, isMobile, gate]) const onPageSelected = React.useCallback( (index: number) => { @@ -231,7 +243,12 @@ function HomeScreenReady({ onPageSelected={onPageSelected} onPageScrollStateChanged={onPageScrollStateChanged} renderTabBar={renderTabBar}> - + ) } diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx index eb3b270488..b7ce8cdd00 100644 --- a/src/view/screens/ModerationBlockedAccounts.tsx +++ b/src/view/screens/ModerationBlockedAccounts.tsx @@ -7,23 +7,26 @@ import { View, } from 'react-native' import {AppBskyActorDefs as ActorDefs} from '@atproto/api' -import {Text} from '../com/util/text/Text' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {cleanError} from '#/lib/strings/errors' +import {logger} from '#/logger' +import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts' +import {useSetMinimalShellMode} from '#/state/shell' +import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {NativeStackScreenProps} from '@react-navigation/native-stack' import {CommonNavigatorParams} from 'lib/routes/types' -import {useAnalytics} from 'lib/analytics/analytics' -import {useFocusEffect} from '@react-navigation/native' -import {ViewHeader} from '../com/util/ViewHeader' +import {useGate} from 'lib/statsig/statsig' +import {isWeb} from 'platform/detection' +import {ProfileCard} from 'view/com/profile/ProfileCard' import {CenteredView} from 'view/com/util/Views' import {ErrorScreen} from '../com/util/error/ErrorScreen' -import {ProfileCard} from 'view/com/profile/ProfileCard' -import {logger} from '#/logger' -import {useSetMinimalShellMode} from '#/state/shell' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts' -import {cleanError} from '#/lib/strings/errors' +import {Text} from '../com/util/text/Text' +import {ViewHeader} from '../com/util/ViewHeader' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -35,6 +38,8 @@ export function ModerationBlockedAccounts({}: Props) { const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop} = useWebMediaQueries() const {screen} = useAnalytics() + const gate = useGate() + const [isPTRing, setIsPTRing] = React.useState(false) const { data, @@ -163,6 +168,9 @@ export function ModerationBlockedAccounts({}: Props) { )} // @ts-ignore our .web version only -prf desktopFixedHeight + showsVerticalScrollIndicator={ + isWeb || !gate('hide_vertical_scroll_indicators') + } /> )} diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx index 911ace7782..4d7ca62946 100644 --- a/src/view/screens/ModerationMutedAccounts.tsx +++ b/src/view/screens/ModerationMutedAccounts.tsx @@ -7,23 +7,26 @@ import { View, } from 'react-native' import {AppBskyActorDefs as ActorDefs} from '@atproto/api' -import {Text} from '../com/util/text/Text' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {cleanError} from '#/lib/strings/errors' +import {logger} from '#/logger' +import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts' +import {useSetMinimalShellMode} from '#/state/shell' +import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {NativeStackScreenProps} from '@react-navigation/native-stack' import {CommonNavigatorParams} from 'lib/routes/types' -import {useAnalytics} from 'lib/analytics/analytics' -import {useFocusEffect} from '@react-navigation/native' -import {ViewHeader} from '../com/util/ViewHeader' +import {useGate} from 'lib/statsig/statsig' +import {isWeb} from 'platform/detection' +import {ProfileCard} from 'view/com/profile/ProfileCard' import {CenteredView} from 'view/com/util/Views' import {ErrorScreen} from '../com/util/error/ErrorScreen' -import {ProfileCard} from 'view/com/profile/ProfileCard' -import {logger} from '#/logger' -import {useSetMinimalShellMode} from '#/state/shell' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts' -import {cleanError} from '#/lib/strings/errors' +import {Text} from '../com/util/text/Text' +import {ViewHeader} from '../com/util/ViewHeader' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -35,6 +38,8 @@ export function ModerationMutedAccounts({}: Props) { const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop} = useWebMediaQueries() const {screen} = useAnalytics() + const gate = useGate() + const [isPTRing, setIsPTRing] = React.useState(false) const { data, @@ -162,6 +167,9 @@ export function ModerationMutedAccounts({}: Props) { )} // @ts-ignore our .web version only -prf desktopFixedHeight + showsVerticalScrollIndicator={ + isWeb || !gate('hide_vertical_scroll_indicators') + } /> )} diff --git a/src/view/screens/PreferencesExternalEmbeds.tsx b/src/view/screens/PreferencesExternalEmbeds.tsx index 1e8cedf7e2..5eec7e5077 100644 --- a/src/view/screens/PreferencesExternalEmbeds.tsx +++ b/src/view/screens/PreferencesExternalEmbeds.tsx @@ -1,25 +1,26 @@ import React from 'react' import {StyleSheet, View} from 'react-native' +import {Trans} from '@lingui/macro' import {useFocusEffect} from '@react-navigation/native' -import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' -import {s} from 'lib/styles' -import {Text} from '../com/util/text/Text' -import {usePalette} from 'lib/hooks/usePalette' -import {useAnalytics} from 'lib/analytics/analytics' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' + import { EmbedPlayerSource, externalEmbedLabels, } from '#/lib/strings/embed-player' import {useSetMinimalShellMode} from '#/state/shell' -import {Trans} from '@lingui/macro' -import {ScrollView} from '../com/util/Views' +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 { useExternalEmbedsPrefs, useSetExternalEmbedPref, } 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' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -74,13 +75,16 @@ export function PreferencesExternalEmbeds({}: Props) { Enable media players for - {Object.entries(externalEmbedLabels).map(([key, label]) => ( - - ))} + {Object.entries(externalEmbedLabels) + // TODO: Remove special case when we disable the old integration. + .filter(([key]) => key !== 'tenor') + .map(([key, label]) => ( + + ))} ) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 6073b95716..02d7c90fb4 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -12,15 +12,13 @@ import {useFocusEffect} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import {cleanError} from '#/lib/strings/errors' -import {isInvalidHandle} from '#/lib/strings/handles' import {useProfileShadow} from '#/state/cache/profile-shadow' -import {listenSoftReset} from '#/state/events' import {useLabelerInfoQuery} from '#/state/queries/labeler' import {resetProfilePostsQueries} from '#/state/queries/post-feed' import {useModerationOpts} from '#/state/queries/preferences' import {useProfileQuery} from '#/state/queries/profile' import {useResolveDidQuery} from '#/state/queries/resolve-uri' -import {getAgent, useSession} from '#/state/session' +import {useAgent, useSession} from '#/state/session' import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' import {useComposerControls} from '#/state/shell/composer' import {useAnalytics} from 'lib/analytics/analytics' @@ -28,12 +26,15 @@ import {useSetTitle} from 'lib/hooks/useSetTitle' import {ComposeIcon2} from 'lib/icons' import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import {combinedDisplayName} from 'lib/strings/display-names' +import {isInvalidHandle} from 'lib/strings/handles' import {colors, s} from 'lib/styles' +import {listenSoftReset} from 'state/events' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' import {ScreenHider} from '#/components/moderation/ScreenHider' +import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder' import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens' import {ProfileLists} from '../com/lists/ProfileLists' import {ErrorScreen} from '../com/util/error/ErrorScreen' @@ -152,6 +153,9 @@ function ProfileScreenLoaded({ const [currentPage, setCurrentPage] = React.useState(0) const {_} = useLingui() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() + + const [scrollViewTag, setScrollViewTag] = React.useState(null) + const postsSectionRef = React.useRef(null) const repliesSectionRef = React.useRef(null) const mediaSectionRef = React.useRef(null) @@ -178,8 +182,7 @@ function ProfileScreenLoaded({ const showRepliesTab = hasSession const showMediaTab = !hasLabeler const showLikesTab = isMe - const showFeedsTab = - hasSession && (isMe || (profile.associated?.feedgens || 0) > 0) + const showFeedsTab = isMe || (profile.associated?.feedgens || 0) > 0 const showListsTab = hasSession && (isMe || (profile.associated?.lists || 0) > 0) @@ -297,12 +300,9 @@ function ProfileScreenLoaded({ openComposer({mention}) }, [openComposer, currentAccount, track, profile]) - const onPageSelected = React.useCallback( - (i: number) => { - setCurrentPage(i) - }, - [setCurrentPage], - ) + const onPageSelected = React.useCallback((i: number) => { + setCurrentPage(i) + }, []) const onCurrentPageSelected = React.useCallback( (index: number) => { @@ -316,20 +316,23 @@ function ProfileScreenLoaded({ const renderHeader = React.useCallback(() => { return ( - + + + ) }, [ + scrollViewTag, profile, labelerInfo, - descriptionRT, hasDescription, + descriptionRT, moderationOpts, hideBackButton, showPlaceholder, @@ -349,7 +352,7 @@ function ProfileScreenLoaded({ onCurrentPageSelected={onCurrentPageSelected} renderHeader={renderHeader}> {showFiltersTab - ? ({headerHeight, scrollElRef}) => ( + ? ({headerHeight, isFocused, scrollElRef}) => ( ) : null} @@ -369,6 +374,7 @@ function ProfileScreenLoaded({ scrollElRef={scrollElRef as ListRef} headerOffset={headerHeight} enabled={isFocused} + setScrollViewTag={setScrollViewTag} /> ) : null} @@ -381,6 +387,7 @@ function ProfileScreenLoaded({ isFocused={isFocused} scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} + setScrollViewTag={setScrollViewTag} /> ) : null} @@ -393,6 +400,7 @@ function ProfileScreenLoaded({ isFocused={isFocused} scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} + setScrollViewTag={setScrollViewTag} /> ) : null} @@ -405,6 +413,7 @@ function ProfileScreenLoaded({ isFocused={isFocused} scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} + setScrollViewTag={setScrollViewTag} /> ) : null} @@ -417,6 +426,7 @@ function ProfileScreenLoaded({ isFocused={isFocused} scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} + setScrollViewTag={setScrollViewTag} /> ) : null} @@ -428,6 +438,7 @@ function ProfileScreenLoaded({ scrollElRef={scrollElRef as ListRef} headerOffset={headerHeight} enabled={isFocused} + setScrollViewTag={setScrollViewTag} /> ) : null} @@ -439,6 +450,7 @@ function ProfileScreenLoaded({ scrollElRef={scrollElRef as ListRef} headerOffset={headerHeight} enabled={isFocused} + setScrollViewTag={setScrollViewTag} /> ) : null} @@ -458,6 +470,7 @@ function ProfileScreenLoaded({ } function useRichText(text: string): [RichTextAPI, boolean] { + const {getAgent} = useAgent() const [prevText, setPrevText] = React.useState(text) const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text})) const [resolvedRT, setResolvedRT] = React.useState(null) @@ -481,7 +494,7 @@ function useRichText(text: string): [RichTextAPI, boolean] { return () => { ignore = true } - }, [text]) + }, [text, getAgent]) const isResolving = resolvedRT === null return [resolvedRT ?? rawRT, isResolving] } diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 4560e14ebc..814c1e8558 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -27,7 +27,7 @@ import {truncateAndInvalidate} from '#/state/queries/util' import {useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {useAnalytics} from 'lib/analytics/analytics' -import {Haptics} from 'lib/haptics' +import {useHaptics} from 'lib/haptics' import {usePalette} from 'lib/hooks/usePalette' import {useSetTitle} from 'lib/hooks/useSetTitle' import {ComposeIcon2} from 'lib/icons' @@ -159,6 +159,7 @@ export function ProfileFeedScreenInner({ const reportDialogControl = useReportDialogControl() const {openComposer} = useComposerControls() const {track} = useAnalytics() + const playHaptic = useHaptics() const feedSectionRef = React.useRef(null) const isScreenFocused = useIsFocused() @@ -201,7 +202,7 @@ export function ProfileFeedScreenInner({ const onToggleSaved = React.useCallback(async () => { try { - Haptics.default() + playHaptic() if (isSaved) { await removeFeed({uri: feedInfo.uri}) @@ -221,18 +222,19 @@ export function ProfileFeedScreenInner({ logger.error('Failed up update feeds', {message: err}) } }, [ - feedInfo, + playHaptic, isSaved, - saveFeed, removeFeed, - resetSaveFeed, + feedInfo, resetRemoveFeed, _, + saveFeed, + resetSaveFeed, ]) const onTogglePinned = React.useCallback(async () => { try { - Haptics.default() + playHaptic() if (isPinned) { await unpinFeed({uri: feedInfo.uri}) @@ -245,7 +247,16 @@ export function ProfileFeedScreenInner({ Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to toggle pinned feed', {message: e}) } - }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed, _]) + }, [ + playHaptic, + isPinned, + unpinFeed, + feedInfo, + resetUnpinFeed, + pinFeed, + resetPinFeed, + _, + ]) const onPressShare = React.useCallback(() => { const url = toShareUrl(feedInfo.route.href) @@ -517,6 +528,7 @@ function AboutSection({ const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) const {hasSession} = useSession() const {track} = useAnalytics() + const playHaptic = useHaptics() const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation() const {mutateAsync: unlikeFeed, isPending: isUnlikePending} = useUnlikeMutation() @@ -527,7 +539,7 @@ function AboutSection({ const onToggleLiked = React.useCallback(async () => { try { - Haptics.default() + playHaptic() if (isLiked && likeUri) { await unlikeFeed({uri: likeUri}) @@ -546,7 +558,7 @@ function AboutSection({ ) logger.error('Failed up toggle like', {message: err}) } - }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _]) + }, [playHaptic, isLiked, likeUri, unlikeFeed, track, likeFeed, feedInfo, _]) return ( diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 58b89f2399..1d93a9fd7d 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -1,69 +1,70 @@ import React, {useCallback, useMemo} from 'react' import {Pressable, StyleSheet, View} from 'react-native' +import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useFocusEffect, useIsFocused} from '@react-navigation/native' -import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {useNavigation} from '@react-navigation/native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api' import {useQueryClient} from '@tanstack/react-query' -import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' -import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' -import {Feed} from 'view/com/posts/Feed' -import {Text} from 'view/com/util/text/Text' -import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' -import {CenteredView} from 'view/com/util/Views' -import {EmptyState} from 'view/com/util/EmptyState' -import {LoadingScreen} from 'view/com/util/LoadingScreen' -import {RichText} from '#/components/RichText' -import {Button} from 'view/com/util/forms/Button' -import {TextLink} from 'view/com/util/Link' -import {ListRef} from 'view/com/util/List' -import * as Toast from 'view/com/util/Toast' -import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' -import {FAB} from 'view/com/util/fab/FAB' -import {Haptics} from 'lib/haptics' -import {FeedDescriptor} from '#/state/queries/post-feed' -import {usePalette} from 'lib/hooks/usePalette' -import {useSetTitle} from 'lib/hooks/useSetTitle' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' -import {NavigationProp} from 'lib/routes/types' -import {toShareUrl} from 'lib/strings/url-helpers' -import {shareUrl} from 'lib/sharing' -import {s} from 'lib/styles' -import {sanitizeHandle} from 'lib/strings/handles' -import {makeProfileLink, makeListLink} from 'lib/routes/links' -import {ComposeIcon2} from 'lib/icons' -import {ListMembers} from '#/view/com/lists/ListMembers' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useSetMinimalShellMode} from '#/state/shell' + +import {useAnalytics} from '#/lib/analytics/analytics' +import {cleanError} from '#/lib/strings/errors' +import {logger} from '#/logger' +import {isNative, isWeb} from '#/platform/detection' +import {listenSoftReset} from '#/state/events' import {useModalControls} from '#/state/modals' -import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' -import {useResolveUriQuery} from '#/state/queries/resolve-uri' import { - useListQuery, - useListMuteMutation, useListBlockMutation, useListDeleteMutation, + useListMuteMutation, + useListQuery, } from '#/state/queries/list' -import {cleanError} from '#/lib/strings/errors' -import {useSession} from '#/state/session' -import {useComposerControls} from '#/state/shell/composer' -import {isNative, isWeb} from '#/platform/detection' -import {truncateAndInvalidate} from '#/state/queries/util' +import {FeedDescriptor} from '#/state/queries/post-feed' +import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import { - usePreferencesQuery, usePinFeedMutation, - useUnpinFeedMutation, + usePreferencesQuery, useSetSaveFeedsMutation, + useUnpinFeedMutation, } from '#/state/queries/preferences' -import {logger} from '#/logger' -import {useAnalytics} from '#/lib/analytics/analytics' -import {listenSoftReset} from '#/state/events' +import {useResolveUriQuery} from '#/state/queries/resolve-uri' +import {truncateAndInvalidate} from '#/state/queries/util' +import {useSession} from '#/state/session' +import {useSetMinimalShellMode} from '#/state/shell' +import {useComposerControls} from '#/state/shell/composer' +import {useHaptics} from 'lib/haptics' +import {usePalette} from 'lib/hooks/usePalette' +import {useSetTitle} from 'lib/hooks/useSetTitle' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {ComposeIcon2} from 'lib/icons' +import {makeListLink, makeProfileLink} from 'lib/routes/links' +import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' +import {NavigationProp} from 'lib/routes/types' +import {shareUrl} from 'lib/sharing' +import {sanitizeHandle} from 'lib/strings/handles' +import {toShareUrl} from 'lib/strings/url-helpers' +import {s} from 'lib/styles' +import {ListMembers} from '#/view/com/lists/ListMembers' +import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' +import {Feed} from 'view/com/posts/Feed' +import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' +import {EmptyState} from 'view/com/util/EmptyState' +import {FAB} from 'view/com/util/fab/FAB' +import {Button} from 'view/com/util/forms/Button' +import {DropdownItem, NativeDropdown} from 'view/com/util/forms/NativeDropdown' +import {TextLink} from 'view/com/util/Link' +import {ListRef} from 'view/com/util/List' +import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' +import {LoadingScreen} from 'view/com/util/LoadingScreen' +import {Text} from 'view/com/util/text/Text' +import * as Toast from 'view/com/util/Toast' +import {CenteredView} from 'view/com/util/Views' import {atoms as a, useTheme} from '#/alf' -import * as Prompt from '#/components/Prompt' import {useDialogControl} from '#/components/Dialog' +import * as Prompt from '#/components/Prompt' +import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' +import {RichText} from '#/components/RichText' const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_MOD = ['About'] @@ -254,6 +255,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { const {data: preferences} = usePreferencesQuery() const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() const {track} = useAnalytics() + const playHaptic = useHaptics() const deleteListPromptControl = useDialogControl() const subscribeMutePromptControl = useDialogControl() @@ -263,7 +265,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { const isSaved = preferences?.feeds?.saved?.includes(list.uri) const onTogglePinned = React.useCallback(async () => { - Haptics.default() + playHaptic() try { if (isPinned) { @@ -275,7 +277,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to toggle pinned feed', {message: e}) } - }, [list.uri, isPinned, pinFeed, unpinFeed, _]) + }, [playHaptic, isPinned, unpinFeed, list.uri, pinFeed, _]) const onSubscribeMute = useCallback(async () => { try { diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 251c706384..0003dbd5d9 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -1,31 +1,32 @@ import React from 'react' -import {StyleSheet, View, ActivityIndicator, Pressable} from 'react-native' +import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' + import {track} from '#/lib/analytics/analytics' -import {useAnalytics} from 'lib/analytics/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {CommonNavigatorParams} from 'lib/routes/types' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {ViewHeader} from 'view/com/util/ViewHeader' -import {ScrollView, CenteredView} from 'view/com/util/Views' -import {Text} from 'view/com/util/text/Text' -import {s, colors} from 'lib/styles' -import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import * as Toast from 'view/com/util/Toast' -import {Haptics} from 'lib/haptics' -import {TextLink} from 'view/com/util/Link' import {logger} from '#/logger' -import {useSetMinimalShellMode} from '#/state/shell' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' import { - usePreferencesQuery, usePinFeedMutation, - useUnpinFeedMutation, + usePreferencesQuery, useSetSaveFeedsMutation, + useUnpinFeedMutation, } from '#/state/queries/preferences' +import {useSetMinimalShellMode} from '#/state/shell' +import {useAnalytics} from 'lib/analytics/analytics' +import {useHaptics} from 'lib/haptics' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {CommonNavigatorParams} from 'lib/routes/types' +import {colors, s} from 'lib/styles' +import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' +import {TextLink} from 'view/com/util/Link' +import {Text} from 'view/com/util/text/Text' +import * as Toast from 'view/com/util/Toast' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {CenteredView, ScrollView} from 'view/com/util/Views' const HITSLOP_TOP = { top: 20, @@ -189,13 +190,14 @@ function ListItem({ }) { const pal = usePalette('default') const {_} = useLingui() + const playHaptic = useHaptics() const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation() const {isPending: isUnpinPending, mutateAsync: unpinFeed} = useUnpinFeedMutation() const isPending = isPinPending || isUnpinPending const onTogglePinned = React.useCallback(async () => { - Haptics.default() + playHaptic() try { resetSaveFeedsMutationState() @@ -209,7 +211,15 @@ function ListItem({ Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to toggle pinned feed', {message: e}) } - }, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState, _]) + }, [ + playHaptic, + resetSaveFeedsMutationState, + isPinned, + unpinFeed, + feedUri, + pinFeed, + _, + ]) const onPressUp = React.useCallback(async () => { if (!isPinned) return diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index 0f24252ce6..9355c2d60e 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -7,6 +7,13 @@ import { TextInput, View, } from 'react-native' +import Animated, { + FadeIn, + FadeOut, + LinearTransition, + useAnimatedStyle, + withSpring, +} from 'react-native-reanimated' import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api' import { FontAwesomeIcon, @@ -22,20 +29,20 @@ import {HITSLOP_10} from '#/lib/constants' import {usePalette} from '#/lib/hooks/usePalette' import {MagnifyingGlassIcon} from '#/lib/icons' import {NavigationProp} from '#/lib/routes/types' -import {useGate} from '#/lib/statsig/statsig' import {augmentSearchQuery} from '#/lib/strings/helpers' import {s} from '#/lib/styles' import {logger} from '#/logger' -import {isNative, isWeb} from '#/platform/detection' +import {isIOS, isNative, isWeb} from '#/platform/detection' import {listenSoftReset} from '#/state/events' -import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' +import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useActorSearch} from '#/state/queries/actor-search' import {useModerationOpts} from '#/state/queries/preferences' import {useSearchPostsQuery} from '#/state/queries/search-posts' -import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows' +import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' import {useSession} from '#/state/session' import {useSetDrawerOpen} from '#/state/shell' import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' +import {useNonReactiveCallback} from 'lib/hooks/useNonReactiveCallback' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import { NativeStackScreenProps, @@ -56,6 +63,7 @@ import { } from '#/view/shell/desktop/Search' import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {atoms as a} from '#/alf' +const AnimatedPressable = Animated.createAnimatedComponent(Pressable) function Loader() { const pal = usePalette('default') @@ -118,49 +126,47 @@ function EmptyState({message, error}: {message: string; error?: string}) { ) } -function SearchScreenSuggestedFollows() { - const pal = usePalette('default') - const {currentAccount} = useSession() - const [suggestions, setSuggestions] = React.useState< - AppBskyActorDefs.ProfileViewBasic[] - >([]) - const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor() - - React.useEffect(() => { - async function getSuggestions() { - const friends = await getSuggestedFollowsByActor( - currentAccount!.did, - ).then(friendsRes => friendsRes.suggestions) - - if (!friends) return // :( - - const friendsOfFriends = new Map< - string, - AppBskyActorDefs.ProfileViewBasic - >() - - await Promise.all( - friends.slice(0, 4).map(friend => - getSuggestedFollowsByActor(friend.did).then(foafsRes => { - for (const user of foafsRes.suggestions) { - if (user.associated?.labeler) continue - friendsOfFriends.set(user.did, user) - } - }), - ), - ) - - setSuggestions(Array.from(friendsOfFriends.values())) - } +function useSuggestedFollows(): [ + AppBskyActorDefs.ProfileViewBasic[], + () => void, +] { + const { + data: suggestions, + hasNextPage, + isFetchingNextPage, + isError, + fetchNextPage, + } = useSuggestedFollowsQuery() + const onEndReached = React.useCallback(async () => { + if (isFetchingNextPage || !hasNextPage || isError) return try { - getSuggestions() - } catch (e) { - logger.error(`SearchScreenSuggestedFollows: failed to get suggestions`, { - message: e, - }) + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more suggested follows', {message: err}) } - }, [currentAccount, setSuggestions, getSuggestedFollowsByActor]) + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) + + const items: AppBskyActorDefs.ProfileViewBasic[] = [] + if (suggestions) { + // Currently the responses contain duplicate items. + // Needs to be fixed on backend, but let's dedupe to be safe. + let seen = new Set() + for (const page of suggestions.pages) { + for (const actor of page.actors) { + if (!seen.has(actor.did)) { + seen.add(actor.did) + items.push(actor) + } + } + } + } + return [items, onEndReached] +} + +function SearchScreenSuggestedFollows() { + const pal = usePalette('default') + const [suggestions, onEndReached] = useSuggestedFollows() return suggestions.length ? ( item.did} // @ts-ignore web only -prf desktopFixedHeight - contentContainerStyle={{paddingBottom: 1200}} + contentContainerStyle={{paddingBottom: 200}} keyboardShouldPersistTaps="handled" keyboardDismissMode="on-drag" + onEndReached={onEndReached} + onEndReachedThreshold={2} /> ) : ( @@ -195,9 +203,11 @@ type SearchResultSlice = function SearchScreenPostResults({ query, sort, + active, }: { query: string sort?: 'top' | 'latest' + active: boolean }) { const {_} = useLingui() const {currentAccount} = useSession() @@ -216,7 +226,7 @@ function SearchScreenPostResults({ fetchNextPage, isFetchingNextPage, hasNextPage, - } = useSearchPostsQuery({query: augmentedQuery, sort}) + } = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active}) const onPullToRefresh = React.useCallback(async () => { setIsPTR(true) @@ -297,9 +307,19 @@ function SearchScreenPostResults({ ) } -function SearchScreenUserResults({query}: {query: string}) { +function SearchScreenUserResults({ + query, + active, +}: { + query: string + active: boolean +}) { const {_} = useLingui() - const {data: results, isFetched} = useActorSearch(query) + + const {data: results, isFetched} = useActorSearch({ + query: query, + enabled: active, + }) return isFetched && results ? ( <> @@ -323,118 +343,55 @@ function SearchScreenUserResults({query}: {query: string}) { ) } -export function SearchScreenInner({ - query, - primarySearch, -}: { - query?: string - primarySearch?: boolean -}) { +export function SearchScreenInner({query}: {query?: string}) { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() const {hasSession} = useSession() const {isDesktop} = useWebMediaQueries() + const [activeTab, setActiveTab] = React.useState(0) const {_} = useLingui() - const isNewSearch = useGate('new_search') - const onPageSelected = React.useCallback( (index: number) => { setMinimalShellMode(false) setDrawerSwipeDisabled(index > 0) + setActiveTab(index) }, [setDrawerSwipeDisabled, setMinimalShellMode], ) const sections = React.useMemo(() => { if (!query) return [] - if (isNewSearch) { - if (hasSession) { - return [ - { - title: _(msg`Top`), - component: , - }, - { - title: _(msg`Latest`), - component: , - }, - { - title: _(msg`People`), - component: , - }, - ] - } else { - return [ - { - title: _(msg`People`), - component: , - }, - ] - } - } else { - if (hasSession) { - return [ - { - title: _(msg`Posts`), - component: , - }, - { - title: _(msg`Users`), - component: , - }, - ] - } else { - return [ - { - title: _(msg`Users`), - component: , - }, - ] - } - } - }, [hasSession, isNewSearch, _, query]) - - if (hasSession) { - return query ? ( - ( - - section.title)} {...props} /> - - )} - initialPage={0}> - {sections.map((section, i) => ( - {section.component} - ))} - - ) : ( - - - - Suggested Follows - - - - - - ) - } + return [ + { + title: _(msg`Top`), + component: ( + + ), + }, + { + title: _(msg`Latest`), + component: ( + + ), + }, + { + title: _(msg`People`), + component: ( + + ), + }, + ] + }, [_, query, activeTab]) return query ? ( {section.component} ))} + ) : hasSession ? ( + + + + Suggested Follows + + + + + ) : ( - {isDesktop && !primarySearch ? ( - Find users with the search tool on the right - ) : ( - Find users on Bluesky - )} + Find posts and users on Bluesky @@ -513,43 +487,25 @@ export function SearchScreen( const {track} = useAnalytics() const setDrawerOpen = useSetDrawerOpen() const moderationOpts = useModerationOpts() - const search = useActorAutocompleteFn() const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop, isTabletOrMobile} = useWebMediaQueries() - const searchDebounceTimeout = React.useRef( - undefined, - ) - const [isFetching, setIsFetching] = React.useState(false) - const [query, setQuery] = React.useState(props.route?.params?.q || '') - const [searchResults, setSearchResults] = React.useState< - AppBskyActorDefs.ProfileViewBasic[] - >([]) - const [inputIsFocused, setInputIsFocused] = React.useState(false) - const [showAutocompleteResults, setShowAutocompleteResults] = - React.useState(false) - const [searchHistory, setSearchHistory] = React.useState([]) - - /** - * The Search screen's `q` param - */ - const queryParam = props.route?.params?.q + // Query terms + const queryParam = props.route?.params?.q ?? '' + const [searchText, setSearchText] = React.useState(queryParam) + const {data: autocompleteData, isFetching: isAutocompleteFetching} = + useActorAutocompleteQuery(searchText, true) - /** - * If `true`, this means we received new instructions from the router. This - * is handled in a effect, and used to update the value of `query` locally - * within this screen. - */ - const routeParamsMismatch = queryParam && queryParam !== query + const [showAutocomplete, setShowAutocomplete] = React.useState(false) + const [searchHistory, setSearchHistory] = React.useState([]) - React.useEffect(() => { - if (queryParam && routeParamsMismatch) { - // reset immediately and let local state take over - navigation.setParams({q: ''}) - // update query for next search - setQuery(queryParam) - } - }, [queryParam, routeParamsMismatch, navigation]) + useFocusEffect( + useNonReactiveCallback(() => { + if (isWeb) { + setSearchText(queryParam) + } + }), + ) React.useEffect(() => { const loadSearchHistory = async () => { @@ -571,60 +527,32 @@ export function SearchScreen( setDrawerOpen(true) }, [track, setDrawerOpen]) + const onPressClearQuery = React.useCallback(() => { + scrollToTopWeb() + setSearchText('') + textInput.current?.focus() + }, []) + const onPressCancelSearch = React.useCallback(() => { scrollToTopWeb() textInput.current?.blur() - setQuery('') - setShowAutocompleteResults(false) - if (searchDebounceTimeout.current) - clearTimeout(searchDebounceTimeout.current) - }, [textInput]) + setShowAutocomplete(false) + setSearchText(queryParam) + }, [queryParam]) - const onPressClearQuery = React.useCallback(() => { + const onChangeText = React.useCallback(async (text: string) => { scrollToTopWeb() - setQuery('') - setShowAutocompleteResults(false) - }, [setQuery]) - - const onChangeText = React.useCallback( - async (text: string) => { - scrollToTopWeb() - - setQuery(text) - - if (text.length > 0) { - setIsFetching(true) - setShowAutocompleteResults(true) - - if (searchDebounceTimeout.current) { - clearTimeout(searchDebounceTimeout.current) - } - - searchDebounceTimeout.current = setTimeout(async () => { - const results = await search({query: text, limit: 30}) - - if (results) { - setSearchResults(results) - setIsFetching(false) - } - }, 300) - } else { - if (searchDebounceTimeout.current) { - clearTimeout(searchDebounceTimeout.current) - } - setSearchResults([]) - setIsFetching(false) - setShowAutocompleteResults(false) - } - }, - [setQuery, search, setSearchResults], - ) + setSearchText(text) + }, []) const updateSearchHistory = React.useCallback( async (newQuery: string) => { newQuery = newQuery.trim() - if (newQuery && !searchHistory.includes(newQuery)) { - let newHistory = [newQuery, ...searchHistory] + if (newQuery) { + let newHistory = [ + newQuery, + ...searchHistory.filter(q => q !== newQuery), + ] if (newHistory.length > 5) { newHistory = newHistory.slice(0, 5) @@ -644,11 +572,30 @@ export function SearchScreen( [searchHistory, setSearchHistory], ) + const navigateToItem = React.useCallback( + (item: string) => { + scrollToTopWeb() + setShowAutocomplete(false) + updateSearchHistory(item) + + if (isWeb) { + navigation.push('Search', {q: item}) + } else { + textInput.current?.blur() + navigation.setParams({q: item}) + } + }, + [updateSearchHistory, navigation], + ) + const onSubmit = React.useCallback(() => { - scrollToTopWeb() - setShowAutocompleteResults(false) - updateSearchHistory(query) - }, [query, setShowAutocompleteResults, updateSearchHistory]) + navigateToItem(searchText) + }, [navigateToItem, searchText]) + + const handleHistoryItemClick = (item: string) => { + setSearchText(item) + navigateToItem(item) + } const onSoftReset = React.useCallback(() => { scrollToTopWeb() @@ -656,9 +603,9 @@ export function SearchScreen( }, [onPressCancelSearch]) const queryMaybeHandle = React.useMemo(() => { - const match = MATCH_HANDLE.exec(query) + const match = MATCH_HANDLE.exec(queryParam) return match && match[1] - }, [query]) + }, [queryParam]) useFocusEffect( React.useCallback(() => { @@ -667,11 +614,6 @@ export function SearchScreen( }, [onSoftReset, setMinimalShellMode]), ) - const handleHistoryItemClick = (item: React.SetStateAction) => { - setQuery(item) - onSubmit() - } - const handleRemoveHistoryItem = (itemToRemove: string) => { const updatedHistory = searchHistory.filter(item => item !== itemToRemove) setSearchHistory(updatedHistory) @@ -682,6 +624,14 @@ export function SearchScreen( ) } + const showClearButton = showAutocomplete && searchText.length > 0 + const clearButtonStyle = useAnimatedStyle(() => ({ + opacity: withSpring(showClearButton ? 1 : 0, { + overshootClamping: true, + duration: 50, + }), + })) + return ( )} - + isWeb && { + // @ts-ignore web only + cursor: 'default', + }, + ]} + onPress={() => { + textInput.current?.focus() + }}> setInputIsFocused(true)} - onBlur={() => { - // HACK - // give 100ms to not stop click handlers in the search history - // -prf - setTimeout(() => setInputIsFocused(false), 100) + selectTextOnFocus={isNative} + onFocus={() => { + if (isWeb) { + // Prevent a jump on iPad by ensuring that + // the initial focused render has no result list. + requestAnimationFrame(() => { + setShowAutocomplete(true) + }) + } else { + setShowAutocomplete(true) + if (isIOS) { + // We rely on selectTextOnFocus, but it's broken on iOS: + // https://github.com/facebook/react-native/issues/41988 + textInput.current?.setSelection(0, searchText.length) + // We still rely on selectTextOnFocus for it to be instant on Android. + } + } }} onChangeText={onChangeText} onSubmitEditing={onSubmit} @@ -745,40 +718,44 @@ export function SearchScreen( autoComplete="off" autoCapitalize="none" /> - {query ? ( - - - - ) : undefined} - - - {query || inputIsFocused ? ( - - + + + + {showAutocomplete && ( + + - + Cancel - + - ) : undefined} + )} - {showAutocompleteResults ? ( + {showAutocomplete && searchText.length > 0 ? ( <> - {isFetching || !moderationOpts ? ( + {(isAutocompleteFetching && !autocompleteData?.length) || + !moderationOpts ? ( ) : ( @@ -805,11 +782,18 @@ export function SearchScreen( /> ) : null} - {searchResults.map(item => ( + {autocompleteData?.map(item => ( { + if (isWeb) { + setShowAutocomplete(false) + } else { + textInput.current?.blur() + } + }} /> ))} @@ -817,7 +801,7 @@ export function SearchScreen( )} - ) : !query && inputIsFocused ? ( + ) : !queryParam && showAutocomplete ? ( - ) : routeParamsMismatch ? ( - ) : ( - + )} ) @@ -917,6 +899,9 @@ const styles = StyleSheet.create({ }, headerCancelBtn: { paddingLeft: 10, + alignSelf: 'center', + zIndex: -1, + elevation: -1, // For Android }, tabBarContainer: { // @ts-ignore web only diff --git a/src/view/screens/Settings/DisableEmail2FADialog.tsx b/src/view/screens/Settings/DisableEmail2FADialog.tsx new file mode 100644 index 0000000000..83b133f656 --- /dev/null +++ b/src/view/screens/Settings/DisableEmail2FADialog.tsx @@ -0,0 +1,197 @@ +import React, {useState} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {cleanError} from '#/lib/strings/errors' +import {isNative} from '#/platform/detection' +import {useAgent, useSession, useSessionApi} from '#/state/session' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' +import {Loader} from '#/components/Loader' +import {P, Text} from '#/components/Typography' + +enum Stages { + Email, + ConfirmCode, +} + +export function DisableEmail2FADialog({ + control, +}: { + control: Dialog.DialogOuterProps['control'] +}) { + const {_} = useLingui() + const t = useTheme() + const {gtMobile} = useBreakpoints() + const {currentAccount} = useSession() + const {updateCurrentAccount} = useSessionApi() + const {getAgent} = useAgent() + + const [stage, setStage] = useState(Stages.Email) + const [confirmationCode, setConfirmationCode] = useState('') + const [isProcessing, setIsProcessing] = useState(false) + const [error, setError] = useState('') + + const onSendEmail = async () => { + setError('') + setIsProcessing(true) + try { + await getAgent().com.atproto.server.requestEmailUpdate() + setStage(Stages.ConfirmCode) + } catch (e) { + setError(cleanError(String(e))) + } finally { + setIsProcessing(false) + } + } + + const onConfirmDisable = async () => { + setError('') + setIsProcessing(true) + try { + if (currentAccount?.email) { + await getAgent().com.atproto.server.updateEmail({ + email: currentAccount!.email, + token: confirmationCode.trim(), + emailAuthFactor: false, + }) + updateCurrentAccount({emailAuthFactor: false}) + Toast.show(_(msg`Email 2FA disabled`)) + } + control.close() + } catch (e) { + const errMsg = String(e) + if (errMsg.includes('Token is invalid')) { + setError(_(msg`Invalid 2FA confirmation code.`)) + } else { + setError(cleanError(errMsg)) + } + } finally { + setIsProcessing(false) + } + } + + return ( + + + + + + + Disable Email 2FA + +

+ {stage === Stages.ConfirmCode ? ( + + An email has been sent to{' '} + {currentAccount?.email || '(no email)'}. It includes a + confirmation code which you can enter below. + + ) : ( + + To disable the email 2FA method, please verify your access to + the email address. + + )} +

+ + {error ? : undefined} + + {stage === Stages.Email ? ( + + + + + ) : stage === Stages.ConfirmCode ? ( + + + + Confirmation code + + + + + + + + + + + + ) : undefined} + + {!gtMobile && isNative && } + +
+
+ ) +} diff --git a/src/view/screens/Settings/Email2FAToggle.tsx b/src/view/screens/Settings/Email2FAToggle.tsx new file mode 100644 index 0000000000..87a56ba5eb --- /dev/null +++ b/src/view/screens/Settings/Email2FAToggle.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useModalControls} from '#/state/modals' +import {useAgent, useSession, useSessionApi} from '#/state/session' +import {ToggleButton} from 'view/com/util/forms/ToggleButton' +import {useDialogControl} from '#/components/Dialog' +import {DisableEmail2FADialog} from './DisableEmail2FADialog' + +export function Email2FAToggle() { + const {_} = useLingui() + const {currentAccount} = useSession() + const {updateCurrentAccount} = useSessionApi() + const {openModal} = useModalControls() + const disableDialogCtrl = useDialogControl() + const {getAgent} = useAgent() + + const enableEmailAuthFactor = React.useCallback(async () => { + if (currentAccount?.email) { + await getAgent().com.atproto.server.updateEmail({ + email: currentAccount.email, + emailAuthFactor: true, + }) + updateCurrentAccount({ + emailAuthFactor: true, + }) + } + }, [currentAccount, updateCurrentAccount, getAgent]) + + const onToggle = React.useCallback(() => { + if (!currentAccount) { + return + } + if (currentAccount.emailAuthFactor) { + disableDialogCtrl.open() + } else { + if (!currentAccount.emailConfirmed) { + openModal({ + name: 'verify-email', + onSuccess: enableEmailAuthFactor, + }) + return + } + enableEmailAuthFactor() + } + }, [currentAccount, enableEmailAuthFactor, openModal, disableDialogCtrl]) + + return ( + <> + + + + ) +} diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx index e901fb0905..1b8d430b2a 100644 --- a/src/view/screens/Settings/ExportCarDialog.tsx +++ b/src/view/screens/Settings/ExportCarDialog.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {getAgent, useSession} from '#/state/session' +import {useAgent, useSession} from '#/state/session' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -19,6 +19,7 @@ export function ExportCarDialog({ const t = useTheme() const {gtMobile} = useBreakpoints() const {currentAccount} = useSession() + const {getAgent} = useAgent() const downloadUrl = React.useMemo(() => { const agent = getAgent() @@ -30,7 +31,7 @@ export function ExportCarDialog({ url.pathname = '/xrpc/com.atproto.sync.getRepo' url.searchParams.set('did', agent.session.did) return url.toString() - }, [currentAccount]) + }, [currentAccount, getAgent]) return ( diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index 830a73ff26..6b5390c293 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -23,12 +23,7 @@ import {useQueryClient} from '@tanstack/react-query' import {isNative} from '#/platform/detection' import {useModalControls} from '#/state/modals' import {clearLegacyStorage} from '#/state/persisted/legacy' -// TODO import {useInviteCodesQuery} from '#/state/queries/invites' import {clear as clearStorage} from '#/state/persisted/store' -import { - useRequireAltTextEnabled, - useSetRequireAltTextEnabled, -} from '#/state/preferences' import { useInAppBrowser, useSetInAppBrowser, @@ -68,6 +63,8 @@ import {UserAvatar} from 'view/com/util/UserAvatar' import {ScrollView} from 'view/com/util/Views' import {useDialogControl} from '#/components/Dialog' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' +import {navigate, resetToTab} from '#/Navigation' +import {Email2FAToggle} from './Email2FAToggle' import {ExportCarDialog} from './ExportCarDialog' function SettingsAccountCard({account}: {account: SessionAccount}) { @@ -101,7 +98,14 @@ function SettingsAccountCard({account}: {account: SessionAccount}) { { - logout('Settings') + if (isNative) { + logout('Settings') + resetToTab('HomeTab') + } else { + navigate('Home').then(() => { + logout('Settings') + }) + } }} accessibilityRole="button" accessibilityLabel={_(msg`Sign out`)} @@ -151,8 +155,6 @@ export function SettingsScreen({}: Props) { const pal = usePalette('default') const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() - const requireAltTextEnabled = useRequireAltTextEnabled() - const setRequireAltTextEnabled = useSetRequireAltTextEnabled() const inAppBrowserPref = useInAppBrowser() const setUseInAppBrowser = useSetInAppBrowser() const onboardingDispatch = useOnboardingDispatch() @@ -162,9 +164,6 @@ export function SettingsScreen({}: Props) { const {openModal} = useModalControls() const {isSwitchingAccounts, accounts, currentAccount} = useSession() const {mutate: clearPreferences} = useClearPreferencesMutation() - // TODO - // const {data: invites} = useInviteCodesQuery() - // const invitesAvailable = invites?.available?.length ?? 0 const {setShowLoggedOut} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() const exportCarControl = useDialogControl() @@ -220,13 +219,6 @@ export function SettingsScreen({}: Props) { exportCarControl.open() }, [exportCarControl]) - /* TODO - const onPressInviteCodes = React.useCallback(() => { - track('Settings:InvitecodesButtonClicked') - openModal({name: 'invite-codes'}) - }, [track, openModal]) - */ - const onPressLanguageSettings = React.useCallback(() => { navigation.navigate('LanguageSettings') }, [navigation]) @@ -279,6 +271,10 @@ export function SettingsScreen({}: Props) { navigation.navigate('SavedFeeds') }, [navigation]) + const onPressAccessibilitySettings = React.useCallback(() => { + navigation.navigate('AccessibilitySettings') + }, [navigation]) + const onPressStatusPage = React.useCallback(() => { Linking.openURL(STATUS_PAGE_URL) }, []) @@ -315,7 +311,7 @@ export function SettingsScreen({}: Props) {
- {/* TODO ( - <> - - Invite a Friend - - - - 0 ? primaryBg : pal.btn, - ]}> - 0 - ? primaryText - : pal.text) as FontAwesomeIconStyle - } - /> - - 0 ? pal.link : pal.text}> - {invites?.disabled ? ( - - Your invite codes are hidden when logged in using an App - Password - - ) : invitesAvailable === 1 ? ( - {invitesAvailable} invite code available - ) : ( - {invitesAvailable} invite codes available - )} - - - - - - )*/} - - - Accessibility - - - setRequireAltTextEnabled(!requireAltTextEnabled)} - /> - - - - Appearance @@ -542,107 +471,130 @@ export function SettingsScreen({}: Props) { Basics + accessibilityLabel={_(msg`Accessibility settings`)} + accessibilityHint={_(msg`Opens accessibility settings`)}> - Following Feed Preferences + Accessibility + accessibilityLabel={_(msg`Language settings`)} + accessibilityHint={_(msg`Opens configurable language settings`)}> - Thread Preferences + Languages navigation.navigate('Moderation') + } accessibilityRole="button" - accessibilityLabel={_(msg`My saved feeds`)} - accessibilityHint={_(msg`Opens screen with all saved feeds`)}> + accessibilityLabel={_(msg`Moderation settings`)} + accessibilityHint={_(msg`Opens moderation settings`)}> - + - My Saved Feeds + Moderation + accessibilityLabel={_(msg`Following feed preferences`)} + accessibilityHint={_(msg`Opens the Following feed preferences`)}> - Languages + Following Feed Preferences navigation.navigate('Moderation') - } + onPress={openThreadsPreferences} accessibilityRole="button" - accessibilityLabel={_(msg`Moderation settings`)} - accessibilityHint={_(msg`Opens moderation settings`)}> + accessibilityLabel={_(msg`Thread preferences`)} + accessibilityHint={_(msg`Opens the threads preferences`)}> - + - Moderation + Thread Preferences + + + + + + + + My Saved Feeds @@ -739,6 +691,13 @@ export function SettingsScreen({}: Props) { )} + + Two-factor authentication + + + + + Account diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx index cae8ec3144..b532b0dd16 100644 --- a/src/view/screens/Storybook/Buttons.tsx +++ b/src/view/screens/Storybook/Buttons.tsx @@ -9,7 +9,7 @@ import { ButtonText, ButtonVariant, } from '#/components/Button' -import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' +import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/Arrow' import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {H1} from '#/components/Typography' diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index f68f9f4ddf..6d166d4b64 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -14,6 +14,43 @@ export function Dialogs() { const prompt = Prompt.usePromptControl() const testDialog = Dialog.useDialogControl() const {closeAllDialogs} = useDialogStateControlContext() + const unmountTestDialog = Dialog.useDialogControl() + const [shouldRenderUnmountTest, setShouldRenderUnmountTest] = + React.useState(false) + const unmountTestInterval = React.useRef() + + const onUnmountTestStartPressWithClose = () => { + setShouldRenderUnmountTest(true) + + setTimeout(() => { + unmountTestDialog.open() + }, 1000) + + setTimeout(() => { + unmountTestDialog.close() + }, 4950) + + setInterval(() => { + setShouldRenderUnmountTest(prev => !prev) + }, 5000) + } + + const onUnmountTestStartPressWithoutClose = () => { + setShouldRenderUnmountTest(true) + + setTimeout(() => { + unmountTestDialog.open() + }, 1000) + + setInterval(() => { + setShouldRenderUnmountTest(prev => !prev) + }, 5000) + } + + const onUnmountTestEndPress = () => { + setShouldRenderUnmountTest(false) + clearInterval(unmountTestInterval.current) + } return ( @@ -70,6 +107,33 @@ export function Dialogs() { Open Tester + + + + + + This is a prompt @@ -257,6 +321,17 @@ export function Dialogs() { + + {shouldRenderUnmountTest && ( + + + + +

Unmount Test Dialog

+

Will unmount in about 5 seconds

+
+
+ )}
) } diff --git a/src/view/screens/Storybook/Icons.tsx b/src/view/screens/Storybook/Icons.tsx index 9d7dc0aa8a..bff1fdc9b7 100644 --- a/src/view/screens/Storybook/Icons.tsx +++ b/src/view/screens/Storybook/Icons.tsx @@ -2,11 +2,11 @@ import React from 'react' import {View} from 'react-native' import {atoms as a, useTheme} from '#/alf' -import {H1} from '#/components/Typography' -import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' -import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' +import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/Arrow' import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {Loader} from '#/components/Loader' +import {H1} from '#/components/Typography' export function Icons() { const t = useTheme() diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 1bf5647f66..8145fa4087 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -9,49 +9,48 @@ import { View, ViewStyle, } from 'react-native' -import {useNavigation, StackActions} from '@react-navigation/native' import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {s, colors} from 'lib/styles' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {StackActions, useNavigation} from '@react-navigation/native' + +import {emitSoftReset} from '#/state/events' +import {useUnreadNotifications} from '#/state/queries/notifications/unread' +import {useProfileQuery} from '#/state/queries/profile' +import {SessionAccount, useSession} from '#/state/session' +import {useSetDrawerOpen} from '#/state/shell' +import {useAnalytics} from 'lib/analytics/analytics' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' +import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' +import {usePalette} from 'lib/hooks/usePalette' import { - HomeIcon, - HomeIconSolid, BellIcon, BellIconSolid, - UserIcon, CogIcon, + HashtagIcon, + HomeIcon, + HomeIconSolid, + ListIcon, MagnifyingGlassIcon2, MagnifyingGlassIcon2Solid, + UserIcon, UserIconSolid, - HashtagIcon, - ListIcon, - HandIcon, } from 'lib/icons' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {Text} from 'view/com/util/text/Text' -import {useTheme} from 'lib/ThemeContext' -import {usePalette} from 'lib/hooks/usePalette' -import {useAnalytics} from 'lib/analytics/analytics' -import {pluralize} from 'lib/strings/helpers' import {getTabState, TabState} from 'lib/routes/helpers' import {NavigationProp} from 'lib/routes/types' -import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' +import {pluralize} from 'lib/strings/helpers' +import {colors, s} from 'lib/styles' +import {useTheme} from 'lib/ThemeContext' import {isWeb} from 'platform/detection' -import {formatCountShortOnly} from 'view/com/util/numeric/format' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useSetDrawerOpen} from '#/state/shell' -import {useSession, SessionAccount} from '#/state/session' -import {useProfileQuery} from '#/state/queries/profile' -import {useUnreadNotifications} from '#/state/queries/notifications/unread' -import {emitSoftReset} from '#/state/events' import {NavSignupCard} from '#/view/shell/NavSignupCard' -import {TextLink} from '../com/util/Link' - +import {formatCountShortOnly} from 'view/com/util/numeric/format' +import {Text} from 'view/com/util/text/Text' +import {UserAvatar} from 'view/com/util/UserAvatar' import {useTheme as useAlfTheme} from '#/alf' +import {TextLink} from '../com/util/Link' let DrawerProfileCard = ({ account, @@ -177,12 +176,6 @@ let DrawerContent = ({}: {}): React.ReactNode => { setDrawerOpen(false) }, [navigation, track, setDrawerOpen]) - const onPressModeration = React.useCallback(() => { - track('Menu:ItemClicked', {url: 'Moderation'}) - navigation.navigate('Moderation') - setDrawerOpen(false) - }, [navigation, track, setDrawerOpen]) - const onPressSettings = React.useCallback(() => { track('Menu:ItemClicked', {url: 'Settings'}) navigation.navigate('Settings') @@ -224,7 +217,9 @@ let DrawerContent = ({}: {}): React.ReactNode => { />
) : ( - + + + )} {hasSession ? ( @@ -238,7 +233,6 @@ let DrawerContent = ({}: {}): React.ReactNode => { /> - { ) : ( - + <> + + + + )} @@ -501,25 +499,6 @@ let ListsMenuItem = ({onPress}: {onPress: () => void}): React.ReactNode => { } ListsMenuItem = React.memo(ListsMenuItem) -let ModerationMenuItem = ({ - onPress, -}: { - onPress: () => void -}): React.ReactNode => { - const {_} = useLingui() - const pal = usePalette('default') - return ( - } - label={_(msg`Moderation`)} - accessibilityLabel={_(msg`Moderation`)} - accessibilityHint="" - onPress={onPress} - /> - ) -} -ModerationMenuItem = React.memo(ModerationMenuItem) - let ProfileMenuItem = ({ isActive, onPress, diff --git a/src/view/shell/NavSignupCard.tsx b/src/view/shell/NavSignupCard.tsx index 83d1414984..12bfa7ea05 100644 --- a/src/view/shell/NavSignupCard.tsx +++ b/src/view/shell/NavSignupCard.tsx @@ -3,13 +3,16 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {Text} from '#/view/com/util/text/Text' -import {Button} from '#/view/com/util/forms/Button' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' +import {Button} from '#/view/com/util/forms/Button' +import {Text} from '#/view/com/util/text/Text' import {Logo} from '#/view/icons/Logo' +import {atoms as a} from '#/alf' +import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' +import {Link} from '#/components/Link' let NavSignupCard = ({}: {}): React.ReactNode => { const {_} = useLingui() @@ -35,7 +38,9 @@ let NavSignupCard = ({}: {}): React.ReactNode => { paddingTop: 6, marginBottom: 24, }}> - + + + @@ -43,7 +48,13 @@ let NavSignupCard = ({}: {}): React.ReactNode => { - + + + + + ) } diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index f41631a969..33f713322a 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -8,7 +8,7 @@ import {BottomTabBarProps} from '@react-navigation/bottom-tabs' import {StackActions} from '@react-navigation/native' import {useAnalytics} from '#/lib/analytics/analytics' -import {Haptics} from '#/lib/haptics' +import {useHaptics} from '#/lib/haptics' import {useDedupe} from '#/lib/hooks/useDedupe' import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' @@ -24,6 +24,7 @@ import { } from '#/lib/icons' import {clamp} from '#/lib/numbers' import {getTabState, TabState} from '#/lib/routes/helpers' +import {useGate} from '#/lib/statsig/statsig' import {s} from '#/lib/styles' import {emitSoftReset} from '#/state/events' import {useUnreadNotifications} from '#/state/queries/notifications/unread' @@ -39,9 +40,17 @@ import {Logo} from '#/view/icons/Logo' import {Logotype} from '#/view/icons/Logotype' import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' +import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' +import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeFilled} from '#/components/icons/Envelope' import {styles} from './BottomBarStyles' -type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds' +type TabOptions = + | 'Home' + | 'Search' + | 'Notifications' + | 'MyProfile' + | 'Feeds' + | 'Messages' export function BottomBar({navigation}: BottomTabBarProps) { const {hasSession, currentAccount} = useSession() @@ -50,8 +59,14 @@ export function BottomBar({navigation}: BottomTabBarProps) { const safeAreaInsets = useSafeAreaInsets() const {track} = useAnalytics() const {footerHeight} = useShellLayout() - const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} = - useNavigationTabState() + const { + isAtHome, + isAtSearch, + isAtFeeds, + isAtNotifications, + isAtMyProfile, + isAtMessages, + } = useNavigationTabState() const numUnreadNotifications = useUnreadNotifications() const {footerMinimalShellTransform} = useMinimalShellMode() const {data: profile} = useProfileQuery({did: currentAccount?.did}) @@ -59,6 +74,8 @@ export function BottomBar({navigation}: BottomTabBarProps) { const closeAllActiveElements = useCloseAllActiveElements() const dedupe = useDedupe() const accountSwitchControl = useDialogControl() + const playHaptic = useHaptics() + const gate = useGate() const showSignIn = React.useCallback(() => { closeAllActiveElements() @@ -103,10 +120,14 @@ export function BottomBar({navigation}: BottomTabBarProps) { onPressTab('MyProfile') }, [onPressTab]) + const onPressMessages = React.useCallback(() => { + onPressTab('Messages') + }, [onPressTab]) + const onLongPressProfile = React.useCallback(() => { - Haptics.default() + playHaptic() accountSwitchControl.open() - }, [accountSwitchControl]) + }, [accountSwitchControl, playHaptic]) return ( <> @@ -219,6 +240,28 @@ export function BottomBar({navigation}: BottomTabBarProps) { : `${numUnreadNotifications} unread` } /> + {gate('dms') && ( + + ) : ( + + ) + } + onPress={onPressMessages} + accessibilityRole="tab" + accessibilityLabel={_(msg`Messages`)} + accessibilityHint="" + /> + )} + {icon} {notificationCount ? ( {notificationCount} ) : undefined} - {icon}
) } diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx index f226406f5d..f76df5bd88 100644 --- a/src/view/shell/bottom-bar/BottomBarStyles.tsx +++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx @@ -1,4 +1,5 @@ import {StyleSheet} from 'react-native' + import {colors} from 'lib/styles' export const styles = StyleSheet.create({ @@ -65,6 +66,9 @@ export const styles = StyleSheet.create({ profileIcon: { top: -4, }, + messagesIcon: { + top: 2, + }, onProfile: { borderWidth: 1, borderRadius: 100, diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index b330c4b808..8b316faa5e 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -1,37 +1,41 @@ import React from 'react' -import {usePalette} from 'lib/hooks/usePalette' -import {useNavigationState} from '@react-navigation/native' +import {View} from 'react-native' import Animated from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {getCurrentRoute, isTab} from 'lib/routes/helpers' -import {styles} from './BottomBarStyles' -import {clamp} from 'lib/numbers' +import {useNavigationState} from '@react-navigation/native' + +import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' +import {usePalette} from '#/lib/hooks/usePalette' import { BellIcon, BellIconSolid, + HashtagIcon, HomeIcon, HomeIconSolid, MagnifyingGlassIcon2, MagnifyingGlassIcon2Solid, - HashtagIcon, UserIcon, UserIconSolid, -} from 'lib/icons' -import {Link} from 'view/com/util/Link' -import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' -import {makeProfileLink} from 'lib/routes/links' -import {CommonNavigatorParams} from 'lib/routes/types' +} from '#/lib/icons' +import {clamp} from '#/lib/numbers' +import {getCurrentRoute, isTab} from '#/lib/routes/helpers' +import {makeProfileLink} from '#/lib/routes/links' +import {CommonNavigatorParams} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' +import {s} from '#/lib/styles' import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' import {Button} from '#/view/com/util/forms/Button' import {Text} from '#/view/com/util/text/Text' -import {s} from 'lib/styles' import {Logo} from '#/view/icons/Logo' import {Logotype} from '#/view/icons/Logotype' +import {Link} from 'view/com/util/Link' +import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' +import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeFilled} from '#/components/icons/Envelope' +import {styles} from './BottomBarStyles' export function BottomBarWeb() { const {_} = useLingui() @@ -41,6 +45,7 @@ export function BottomBarWeb() { const {footerMinimalShellTransform} = useMinimalShellMode() const {requestSwitchToAccount} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() + const gate = useGate() const showSignIn = React.useCallback(() => { closeAllActiveElements() @@ -117,6 +122,19 @@ export function BottomBarWeb() { ) }} + {gate('dms') && ( + + {({isActive}) => { + const Icon = isActive ? EnvelopeFilled : Envelope + return ( + + ) + }} + + )} setShowLoggedOut(false)} /> } if (onboardingState.isActive) { - if (NEW_ONBOARDING_ENABLED) { - return - } else { - return - } + return } const newDescriptors: typeof descriptors = {} for (let key in descriptors) { diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 097ca2fbfb..91d20e089e 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -1,52 +1,55 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {PressableWithHover} from 'view/com/util/PressableWithHover' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import { useLinkProps, useNavigation, useNavigationState, } from '@react-navigation/native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {Text} from 'view/com/util/text/Text' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {Link} from 'view/com/util/Link' -import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' + +import {useGate} from '#/lib/statsig/statsig' +import {isInvalidHandle} from '#/lib/strings/handles' +import {emitSoftReset} from '#/state/events' +import {useFetchHandle} from '#/state/queries/handle' +import {useUnreadNotifications} from '#/state/queries/notifications/unread' +import {useProfileQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import {useComposerControls} from '#/state/shell/composer' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {s, colors} from 'lib/styles' import { - HomeIcon, - HomeIconSolid, - MagnifyingGlassIcon2, - MagnifyingGlassIcon2Solid, BellIcon, BellIconSolid, - UserIcon, - UserIconSolid, CogIcon, CogIconSolid, ComposeIcon2, - ListIcon, HashtagIcon, - HandIcon, + HomeIcon, + HomeIconSolid, + ListIcon, + MagnifyingGlassIcon2, + MagnifyingGlassIcon2Solid, + UserIcon, + UserIconSolid, } from 'lib/icons' -import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers' -import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types' -import {router} from '../../../routes' +import {getCurrentRoute, isStateAtTabRoot, isTab} from 'lib/routes/helpers' import {makeProfileLink} from 'lib/routes/links' -import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' -import {useProfileQuery} from '#/state/queries/profile' -import {useSession} from '#/state/session' -import {useUnreadNotifications} from '#/state/queries/notifications/unread' -import {useComposerControls} from '#/state/shell/composer' -import {useFetchHandle} from '#/state/queries/handle' -import {emitSoftReset} from '#/state/events' +import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' +import {colors, s} from 'lib/styles' import {NavSignupCard} from '#/view/shell/NavSignupCard' -import {isInvalidHandle} from '#/lib/strings/handles' +import {Link} from 'view/com/util/Link' +import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' +import {PressableWithHover} from 'view/com/util/PressableWithHover' +import {Text} from 'view/com/util/text/Text' +import {UserAvatar} from 'view/com/util/UserAvatar' +import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' +import {Envelope_Filled_Stroke2_Corner0_Rounded as EnvelopeFilled} from '#/components/icons/Envelope' +import {router} from '../../../routes' function ProfileCard() { const {currentAccount} = useSession() @@ -272,6 +275,7 @@ export function DesktopLeftNav() { const {_} = useLingui() const {isDesktop, isTablet} = useWebMediaQueries() const numUnread = useUnreadNotifications() + const gate = useGate() if (!hasSession && !isDesktop) { return null @@ -327,24 +331,6 @@ export function DesktopLeftNav() { } label={_(msg`Search`)} /> - - } - iconFilled={ - - } - label={_(msg`Feeds`)} - /> + {gate('dms') && ( + } + iconFilled={ + + } + label={_(msg`Messages`)} + /> + )} + + } + iconFilled={ + + } + label={_(msg`Feeds`)} + /> - - } - iconFilled={ - - } - label={_(msg`Moderation`)} - /> void }) { const pal = usePalette('default') + const queryClient = useQueryClient() + + const onPress = React.useCallback(() => { + precacheProfile(queryClient, profile) + onPressInner() + }, [queryClient, profile, onPressInner]) return ( + anchorNoUnderline + onBeforePress={onPress}> () - const searchDebounceTimeout = React.useRef( - undefined, - ) const [isActive, setIsActive] = React.useState(false) - const [isFetching, setIsFetching] = React.useState(false) const [query, setQuery] = React.useState('') - const [searchResults, setSearchResults] = React.useState< - AppBskyActorDefs.ProfileViewBasic[] - >([]) + const {data: autocompleteData, isFetching} = useActorAutocompleteQuery( + query, + true, + ) const moderationOpts = useModerationOpts() - const search = useActorAutocompleteFn() - - const onChangeText = React.useCallback( - async (text: string) => { - setQuery(text) - - if (text.length > 0) { - setIsFetching(true) - setIsActive(true) - if (searchDebounceTimeout.current) - clearTimeout(searchDebounceTimeout.current) - - searchDebounceTimeout.current = setTimeout(async () => { - const results = await search({query: text}) - - if (results) { - setSearchResults(results) - setIsFetching(false) - } - }, 300) - } else { - if (searchDebounceTimeout.current) - clearTimeout(searchDebounceTimeout.current) - setSearchResults([]) - setIsFetching(false) - setIsActive(false) - } - }, - [setQuery, search, setSearchResults], - ) + const onChangeText = React.useCallback((text: string) => { + setQuery(text) + setIsActive(text.length > 0) + }, []) const onPressCancelSearch = React.useCallback(() => { setQuery('') setIsActive(false) - if (searchDebounceTimeout.current) - clearTimeout(searchDebounceTimeout.current) }, [setQuery]) + const onSubmit = React.useCallback(() => { setIsActive(false) if (!query.length) return - setSearchResults([]) - if (searchDebounceTimeout.current) - clearTimeout(searchDebounceTimeout.current) navigation.dispatch(StackActions.push('Search', {q: query})) - }, [query, navigation, setSearchResults]) + }, [query, navigation]) + + const onSearchProfileCardPress = React.useCallback(() => { + setQuery('') + setIsActive(false) + }, []) const queryMaybeHandle = React.useMemo(() => { const match = MATCH_HANDLE.exec(query) @@ -246,7 +229,7 @@ export function DesktopSearch() { {query !== '' && isActive && moderationOpts && ( - {isFetching ? ( + {isFetching && !autocompleteData?.length ? ( @@ -255,7 +238,11 @@ export function DesktopSearch() { 0 + ? {borderBottomWidth: 1} + : undefined + } /> {queryMaybeHandle ? ( @@ -265,11 +252,12 @@ export function DesktopSearch() { /> ) : null} - {searchResults.map(item => ( + {autocompleteData?.map(item => ( ))} diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index f29183095a..f13a8d7dfe 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -1,37 +1,40 @@ import React from 'react' -import {StatusBar} from 'expo-status-bar' import { + BackHandler, DimensionValue, StyleSheet, useWindowDimensions, View, - BackHandler, } from 'react-native' -import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Drawer} from 'react-native-drawer-layout' +import Animated from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import * as NavigationBar from 'expo-navigation-bar' +import {StatusBar} from 'expo-status-bar' import {useNavigationState} from '@react-navigation/native' -import {ModalsContainer} from 'view/com/modals/Modal' -import {Lightbox} from 'view/com/lightbox/Lightbox' -import {ErrorBoundary} from 'view/com/util/ErrorBoundary' -import {DrawerContent} from './Drawer' -import {Composer} from './Composer' -import {useTheme} from 'lib/ThemeContext' -import {usePalette} from 'lib/hooks/usePalette' -import {RoutesContainer, TabsNavigator} from '../../Navigation' -import {isStateAtTabRoot} from 'lib/routes/helpers' + +import {useAgent, useSession} from '#/state/session' import { useIsDrawerOpen, - useSetDrawerOpen, useIsDrawerSwipeDisabled, + useSetDrawerOpen, } from '#/state/shell' -import {isAndroid} from 'platform/detection' -import {useSession} from '#/state/session' import {useCloseAnyActiveElement} from '#/state/util' +import {usePalette} from 'lib/hooks/usePalette' import * as notifications from 'lib/notifications/notifications' -import {Outlet as PortalOutlet} from '#/components/Portal' -import {MutedWordsDialog} from '#/components/dialogs/MutedWords' +import {isStateAtTabRoot} from 'lib/routes/helpers' +import {useTheme} from 'lib/ThemeContext' +import {isAndroid} from 'platform/detection' import {useDialogStateContext} from 'state/dialogs' -import Animated from 'react-native-reanimated' +import {Lightbox} from 'view/com/lightbox/Lightbox' +import {ModalsContainer} from 'view/com/modals/Modal' +import {ErrorBoundary} from 'view/com/util/ErrorBoundary' +import {MutedWordsDialog} from '#/components/dialogs/MutedWords' +import {SigninDialog} from '#/components/dialogs/Signin' +import {Outlet as PortalOutlet} from '#/components/Portal' +import {RoutesContainer, TabsNavigator} from '../../Navigation' +import {Composer} from './Composer' +import {DrawerContent} from './Drawer' function ShellInner() { const isDrawerOpen = useIsDrawerOpen() @@ -54,6 +57,7 @@ function ShellInner() { ) const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) const {hasSession, currentAccount} = useSession() + const {getAgent} = useAgent() const closeAnyActiveElement = useCloseAnyActiveElement() const {importantForAccessibility} = useDialogStateContext() // start undefined @@ -75,11 +79,14 @@ function ShellInner() { // only runs when did changes if (currentAccount && currentAccountDid.current !== currentAccount.did) { currentAccountDid.current = currentAccount.did - notifications.requestPermissionsAndRegisterToken(currentAccount) - const unsub = notifications.registerTokenChangeHandler(currentAccount) + notifications.requestPermissionsAndRegisterToken(getAgent, currentAccount) + const unsub = notifications.registerTokenChangeHandler( + getAgent, + currentAccount, + ) return unsub } - }, [currentAccount]) + }, [currentAccount, getAgent]) return ( <> @@ -101,6 +108,7 @@ function ShellInner() { + @@ -110,6 +118,15 @@ function ShellInner() { export const Shell: React.FC = function ShellImpl() { const pal = usePalette('default') const theme = useTheme() + React.useEffect(() => { + if (isAndroid) { + NavigationBar.setBackgroundColorAsync(theme.palette.default.background) + NavigationBar.setBorderColorAsync(theme.palette.default.background) + NavigationBar.setButtonStyleAsync( + theme.colorScheme === 'dark' ? 'light' : 'dark', + ) + } + }, [theme]) return ( diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 02993ac462..9dab23671f 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -1,24 +1,25 @@ import React, {useEffect} from 'react' -import {View, StyleSheet, TouchableOpacity} from 'react-native' -import {useNavigation} from '@react-navigation/native' +import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' -import {ErrorBoundary} from '../com/util/ErrorBoundary' +import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' +import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' +import {useCloseAllActiveElements} from '#/state/util' +import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' +import {NavigationProp} from 'lib/routes/types' +import {colors, s} from 'lib/styles' +import {MutedWordsDialog} from '#/components/dialogs/MutedWords' +import {SigninDialog} from '#/components/dialogs/Signin' +import {Outlet as PortalOutlet} from '#/components/Portal' +import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries' +import {FlatNavigator, RoutesContainer} from '../../Navigation' import {Lightbox} from '../com/lightbox/Lightbox' import {ModalsContainer} from '../com/modals/Modal' +import {ErrorBoundary} from '../com/util/ErrorBoundary' import {Composer} from './Composer.web' -import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' -import {s, colors} from 'lib/styles' -import {RoutesContainer, FlatNavigator} from '../../Navigation' import {DrawerContent} from './Drawer' -import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries' -import {NavigationProp} from 'lib/routes/types' -import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' -import {useCloseAllActiveElements} from '#/state/util' -import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' -import {Outlet as PortalOutlet} from '#/components/Portal' -import {MutedWordsDialog} from '#/components/dialogs/MutedWords' function ShellInner() { const isDrawerOpen = useIsDrawerOpen() @@ -45,19 +46,26 @@ function ShellInner() { + {!isDesktop && isDrawerOpen && ( - setDrawerOpen(false)} - style={styles.drawerMask} + { + // Only close if press happens outside of the drawer + if (ev.target === ev.currentTarget) { + setDrawerOpen(false) + } + }} accessibilityLabel={_(msg`Close navigation footer`)} accessibilityHint={_(msg`Closes bottom navigation bar`)}> - - + + + + - + )} ) diff --git a/web/index.html b/web/index.html index 06d00dec97..b059e69e90 100644 --- a/web/index.html +++ b/web/index.html @@ -239,6 +239,16 @@ inset:0; animation: rotate 500ms linear infinite; } + + @keyframes avatarHoverFadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + @keyframes avatarHoverFadeOut { + from { opacity: 1; } + to { opacity: 0; } + } diff --git a/webpack.config.js b/webpack.config.js index 6f1de3b8b7..5aa2d47f5b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,5 @@ +const fs = require('fs') +const path = require('path') const createExpoWebpackConfigAsync = require('@expo/webpack-config') const {withAlias} = require('@expo/webpack-config/addons') const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') @@ -22,6 +24,25 @@ module.exports = async function (env, argv) { 'react-native$': 'react-native-web', 'react-native-webview': 'react-native-web-webview', }) + + if (process.env.ATPROTO_ROOT) { + const atprotoRoot = path.resolve(process.cwd(), process.env.ATPROTO_ROOT) + const atprotoPackages = path.join(atprotoRoot, 'packages') + + config = withAlias( + config, + Object.fromEntries( + fs + .readdirSync(atprotoPackages) + .map(pkgName => [pkgName, path.join(atprotoPackages, pkgName)]) + .filter(([_, pkgPath]) => + fs.existsSync(path.join(pkgPath, 'package.json')), + ) + .map(([pkgName, pkgPath]) => [`@atproto/${pkgName}`, pkgPath]), + ), + ) + } + config.module.rules = [ ...(config.module.rules || []), reactNativeWebWebviewConfiguration, diff --git a/yarn.lock b/yarn.lock index f5a3799da4..63b027fed6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,10 +34,22 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@atproto/api@^0.12.2": - version "0.12.2" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.2.tgz#5df6d4f60dea0395c84fdebd9e81a7e853edf130" - integrity sha512-UVzCiDZH2j0wrr/O8nb1edD5cYLVqB5iujueXUCbHS3rAwIxgmyLtA3Hzm2QYsGPo/+xsIg1fNvpq9rNT6KWUA== +"@atproto/api@^0.12.3": + version "0.12.3" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.3.tgz#5b7b1c7d4210ee9315961504900c8409395cbb17" + integrity sha512-y/kGpIEo+mKGQ7VOphpqCAigTI0LZRmDThNChTfSzDKm9TzEobwiw0zUID0Yw6ot1iLLFx3nKURmuZAYlEuobw== + 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/api@^0.12.5": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.5.tgz#3ed70990b27c468d9663ca71306039cab663ca96" + integrity sha512-xqdl/KrAK2kW6hN8+eSmKTWHgMNaPnDAEvZzo08Xbk/5jdRzjoEPS+p7k/wQ+ZefwOHL3QUbVPO4hMfmVxzO/Q== dependencies: "@atproto/common-web" "^0.3.0" "@atproto/lexicon" "^0.4.0" @@ -63,12 +75,12 @@ multiformats "^9.9.0" uint8arrays "3.0.0" -"@atproto/bsky@^0.0.44": - version "0.0.44" - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.44.tgz#990d6061d557cdf891d43656543ebb611f57bd82" - integrity sha512-SVOnvdUlDf9sKI1Tto+IY1tVS4/9VRoTTiI08ezvK9sew9sQVUVurwYI5E3EtAbEi3ukBPZ9+Cuoh3Me65iyjQ== +"@atproto/bsky@^0.0.45": + version "0.0.45" + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.45.tgz#c3083d8038fe8c5ff921d9bcb0b5a043cc840827" + integrity sha512-osWeigdYzQH2vZki+eszCR8ta9zdUB4om79aFmnE+zvxw7HFduwAAbcHf6kmmiLCfaOWvCsYb1wS2i3IC66TAg== dependencies: - "@atproto/api" "^0.12.2" + "@atproto/api" "^0.12.3" "@atproto/common" "^0.4.0" "@atproto/crypto" "^0.4.0" "@atproto/identity" "^0.4.0" @@ -177,20 +189,20 @@ "@noble/hashes" "^1.3.1" uint8arrays "3.0.0" -"@atproto/dev-env@^0.3.4": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.4.tgz#153b7be8268b2dcfc8d0ba4abc5fd60ad7a6e241" - integrity sha512-ix33GBQ1hjesoieTQKx38VGxZWNKeXCnaMdalr0/SAFwaDPCqMOrvUTPCx8VWClgAd0qYMcBM98+0lBTohW1qQ== +"@atproto/dev-env@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.5.tgz#cd13313dbc52131731d039a1d22808ee8193505d" + integrity sha512-dqRNihzX1xIHbWPHmfYsliUUXyZn5FFhCeButrGie5soQmHA4okQJTB1XWDly3mdHLjUM90g+5zjRSAKoui77Q== dependencies: - "@atproto/api" "^0.12.2" - "@atproto/bsky" "^0.0.44" + "@atproto/api" "^0.12.3" + "@atproto/bsky" "^0.0.45" "@atproto/bsync" "^0.0.3" "@atproto/common-web" "^0.3.0" "@atproto/crypto" "^0.4.0" "@atproto/identity" "^0.4.0" "@atproto/lexicon" "^0.4.0" - "@atproto/ozone" "^0.1.6" - "@atproto/pds" "^0.4.13" + "@atproto/ozone" "^0.1.7" + "@atproto/pds" "^0.4.14" "@atproto/syntax" "^0.3.0" "@atproto/xrpc-server" "^0.5.1" "@did-plc/lib" "^0.0.1" @@ -222,12 +234,12 @@ multiformats "^9.9.0" zod "^3.21.4" -"@atproto/ozone@^0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.6.tgz#b54c68360af19bfe6914d74b58759df0729461de" - integrity sha512-uAXhXdO75vU/VVGGrsifZfaq6h7cMbEdS3bH8GCJfgwtxOlCU0elV2YM88GHBfVGJ0ghYKNki+Dhvpe8i+Fe1Q== +"@atproto/ozone@^0.1.7": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.7.tgz#248d88e1acfe56936651754975472d03d047d689" + integrity sha512-vvaV0MFynOzZJcL8m8mEW21o1FFIkP+wHTXEC9LJrL3h03+PMaby8Ujmif6WX5eikhfxvr9xsU/Jxbi/iValuQ== dependencies: - "@atproto/api" "^0.12.2" + "@atproto/api" "^0.12.3" "@atproto/common" "^0.4.0" "@atproto/crypto" "^0.4.0" "@atproto/identity" "^0.4.0" @@ -249,12 +261,12 @@ typed-emitter "^2.1.0" uint8arrays "3.0.0" -"@atproto/pds@^0.4.13": - version "0.4.13" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.13.tgz#9235d2c748d142a06d78da143ff1ad7e150b2d97" - integrity sha512-86fmaSFBP1HML0U85bsYkd06oO6XFFA/+VpRMeABy7cUShvvlkVq8anxp301Qaf89t+AM/tvjICqQ2syW8bgfA== +"@atproto/pds@^0.4.14": + version "0.4.14" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.14.tgz#5b55ef307323bda712f2ddaba5c1fff7740ed91b" + integrity sha512-rqVcvtw5oMuuJIpWZbSSTSx19+JaZyUcg9OEjdlUmyEpToRN88zTEQySEksymrrLQkW/LPRyWGd7WthbGEuEfQ== dependencies: - "@atproto/api" "^0.12.2" + "@atproto/api" "^0.12.3" "@atproto/aws" "^0.2.0" "@atproto/common" "^0.4.0" "@atproto/crypto" "^0.4.0" @@ -3511,6 +3523,13 @@ resolved "https://registry.yarnpkg.com/@flatten-js/interval-tree/-/interval-tree-1.1.2.tgz#fcc891da48bc230392884be01c26fe8c625702e8" integrity sha512-OwLoV9E/XM6b7bes2rSFnGNjyRy7vcoIHFTnmBR2WAaZTf0Fe4EX4GdA65vU1KgFAasti7iRSg2dZfYd1Zt00Q== +"@floating-ui/core@^1.0.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" + integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g== + dependencies: + "@floating-ui/utils" "^0.2.1" + "@floating-ui/core@^1.4.1": version "1.4.1" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.4.1.tgz#0d633f4b76052668afb932492ac452f7ebe97f17" @@ -3526,6 +3545,14 @@ "@floating-ui/core" "^1.4.1" "@floating-ui/utils" "^0.1.1" +"@floating-ui/dom@^1.6.1", "@floating-ui/dom@^1.6.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef" + integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw== + dependencies: + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" + "@floating-ui/react-dom@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.1.tgz#7972a4fc488a8c746cded3cfe603b6057c308a91" @@ -3533,11 +3560,23 @@ dependencies: "@floating-ui/dom" "^1.3.0" +"@floating-ui/react-dom@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d" + integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw== + dependencies: + "@floating-ui/dom" "^1.6.1" + "@floating-ui/utils@^0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.1.tgz#1a5b1959a528e374e8037c4396c3e825d6cf4a83" integrity sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw== +"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== + "@fortawesome/fontawesome-common-types@6.4.2": version "6.4.2" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5" @@ -8577,6 +8616,15 @@ asap@~2.0.3, asap@~2.0.6: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +asn1.js@^4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + asn1.js@^5.0.1: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -9101,6 +9149,11 @@ bn.js@^4.0.0, bn.js@^4.11.8, bn.js@^4.11.9: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== +bn.js@^5.0.0, bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + body-parser@1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" @@ -9197,6 +9250,41 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== +browserify-aes@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-rsa@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" + integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== + dependencies: + bn.js "^5.0.0" + randombytes "^2.0.1" + +browserify-sign@4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e" + integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg== + dependencies: + bn.js "^5.2.1" + browserify-rsa "^4.1.0" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.4" + inherits "^2.0.4" + parse-asn1 "^5.1.6" + readable-stream "^3.6.2" + safe-buffer "^5.2.1" + browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.21.9: version "4.21.10" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" @@ -9242,6 +9330,11 @@ buffer-writer@2.0.0: resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== + buffer@5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" @@ -9601,6 +9694,14 @@ ci-info@^3.2.0, ci-info@^3.3.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + cjs-module-lexer@^1.0.0: version "1.2.3" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" @@ -10041,6 +10142,29 @@ cosmiconfig@^8.0.0: parse-json "^5.2.0" path-type "^4.0.0" +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + create-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" @@ -10906,6 +11030,19 @@ elliptic@^6.4.1: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +elliptic@^6.5.4: + version "6.5.5" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded" + integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + email-validator@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed" @@ -11605,6 +11742,14 @@ events@3.3.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + exec-async@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/exec-async/-/exec-async-2.2.0.tgz#c7c5ad2eef3478d38390c6dd3acfe8af0efc8301" @@ -11825,6 +11970,11 @@ expo-eas-client@~0.11.0: resolved "https://registry.yarnpkg.com/expo-eas-client/-/expo-eas-client-0.11.0.tgz#0f25aa497849cade7ebef55c0631093a87e58b07" integrity sha512-99W0MUGe3U4/MY1E9UeJ4uKNI39mN8/sOGA0Le8XC47MTbwbLoVegHR3C5y2fXLwLn7EpfNxAn5nlxYjY3gD2A== +expo-file-system@^16.0.9: + version "16.0.9" + resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-16.0.9.tgz#cbd6c4b228b60a6b6c71fd1b91fe57299fb24da7" + integrity sha512-3gRPvKVv7/Y7AdD9eHMIdfg5YbUn2zbwKofjsloTI5sEC57SLUFJtbLvUCz9Pk63DaSQ7WIE1JM0EASyvuPbuw== + expo-file-system@~16.0.0: version "16.0.1" resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-16.0.1.tgz#326b7c2f6e53e1a0eaafc9769578aafb3f9c9f43" @@ -11935,6 +12085,14 @@ expo-modules-core@1.11.12: dependencies: invariant "^2.2.4" +expo-navigation-bar@~2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/expo-navigation-bar/-/expo-navigation-bar-2.8.1.tgz#c4152f878d9fb6ca74c90b80e934af76c29b5377" + integrity sha512-aT5G+7SUsXDVPsRwp8fF940ycka1ABb4g3QKvTZN3YP6kMWvsiYEmRqMIJVy0zUr/i6bxBG1ZergkXimWrFt3w== + dependencies: + "@react-native/normalize-color" "^2.0.0" + debug "^4.3.2" + expo-notifications@~0.27.6: version "0.27.6" resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.27.6.tgz#ef7c95504034ac8b5fa360e13f5b037c5bf7e80d" @@ -12979,6 +13137,23 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash-base@~3.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + integrity sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" @@ -15785,6 +15960,15 @@ md5-file@^3.2.3: dependencies: buffer-alloc "^1.1.0" +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + md5@^2.2.1: version "2.3.0" resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" @@ -17014,6 +17198,18 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-asn1@^5.1.6: + version "5.1.7" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06" + integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg== + dependencies: + asn1.js "^4.10.1" + browserify-aes "^1.2.0" + evp_bytestokey "^1.0.3" + hash-base "~3.0" + pbkdf2 "^3.1.2" + safe-buffer "^5.2.1" + parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" @@ -17160,6 +17356,17 @@ pathe@^1.1.0: resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== +pbkdf2@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" + integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + peek-readable@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72" @@ -18448,7 +18655,7 @@ ramda@^0.27.1: resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1" integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA== -randombytes@^2.1.0: +randombytes@^2.0.1, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -18944,7 +19151,7 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -19337,6 +19544,14 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + rn-fetch-blob@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/rn-fetch-blob/-/rn-fetch-blob-0.12.0.tgz#ec610d2f9b3f1065556b58ab9c106eeb256f3cba" @@ -19435,7 +19650,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.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== @@ -19722,6 +19937,14 @@ sf-symbols-typescript@^1.0.0: resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-1.0.0.tgz#94e9210bf27e7583f9749a0d07bd4f4937ea488f" integrity sha512-DkS7q3nN68dEMb4E18HFPDAvyrjDZK9YAQQF2QxeFu9gp2xRDXFMF8qLJ1EmQ/qeEGQmop4lmMM1WtYJTIcCMw== +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -22298,7 +22521,12 @@ zeego@^1.6.2: "@radix-ui/react-dropdown-menu" "^2.0.1" sf-symbols-typescript "^1.0.0" -zod@^3.14.2, zod@^3.20.2, zod@^3.21.4: +zod@^3.14.2, zod@^3.21.4: version "3.22.2" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b" integrity sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg== + +zod@^3.22.4: + version "3.22.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" + integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==