Merge branch 'main' into hailey/oauth

This commit is contained in:
Hailey
2024-04-09 20:33:28 -07:00
committed by GitHub
52 changed files with 19377 additions and 15478 deletions
+28 -11
View File
@@ -62,38 +62,55 @@ jobs:
- name: 🏗️ EAS Build - name: 🏗️ EAS Build
run: yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive run: yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive
- name: 🚀 Deploy - name: ✍️ Rename Testflight bundle
if: ${{ inputs.profile == 'production' }}
run: eas submit -p android --non-interactive --path build.aab
- name: ✍️ Rename bundle
if: ${{ inputs.profile != 'production' }} if: ${{ inputs.profile != 'production' }}
run: mv build.aab build.apk run: mv build.aab build.apk
- name: ⏰ Get a timestamp - name: ⏰ Get a timestamp
id: timestamp id: timestamp
if: ${{ inputs.profile != 'production' }}
uses: nanzm/get-time-action@master uses: nanzm/get-time-action@master
with: with:
format: 'MM-DD-HH-mm-ss' format: 'MM-DD-HH-mm-ss'
- name: 🚀 Upload Artifact - name: 🚀 Upload Production Artifact
id: upload-artifact id: upload-artifact-production
if: ${{ inputs.profile == 'production' }}
uses: actions/upload-artifact@v4
with:
retention-days: 30
compression-level: 6
name: build-${{ steps.timestamp.outputs.time }}.aab
path: build.aab
- name: 🚀 Upload Testflight Artifact
id: upload-artifact-testflight
if: ${{ inputs.profile != 'production' }} if: ${{ inputs.profile != 'production' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
retention-days: 30 retention-days: 30
compression-level: 0 compression-level: 6
name: build-${{ steps.timestamp.outputs.time }}.apk name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk path: build.apk
- name: 🔔 Notify Slack - name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": "Android build is ready for submission. This is a production build! Download the artifact here: ${{ steps.upload-artifact-production.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: 🔔 Notify Slack of Testflight Build
if: ${{ inputs.profile != 'production' }} if: ${{ inputs.profile != 'production' }}
uses: slackapi/slack-github-action@v1.25.0 uses: slackapi/slack-github-action@v1.25.0
with: with:
payload: | payload: |
{ {
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}" "text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact-testflight.outputs.artifact-url }}"
} }
env: env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
+2 -2
View File
@@ -1,5 +1,6 @@
import {createServer as createHTTPServer} from 'node:http' import {createServer as createHTTPServer} from 'node:http'
import {parse} from 'node:url' import {parse} from 'node:url'
import {createServer, TestPDS} from '../jest/test-pds' import {createServer, TestPDS} from '../jest/test-pds'
async function main() { async function main() {
@@ -14,8 +15,7 @@ async function main() {
await server?.close() await server?.close()
console.log('Starting new server') console.log('Starting new server')
const inviteRequired = url?.query && 'invite' in url.query const inviteRequired = url?.query && 'invite' in url.query
const phoneRequired = url?.query && 'phone' in url.query server = await createServer({inviteRequired})
server = await createServer({inviteRequired, phoneRequired})
console.log('Listening at', server.pdsUrl) console.log('Listening at', server.pdsUrl)
if (url?.query) { if (url?.query) {
if ('users' in url.query) { if ('users' in url.query) {
+13 -15
View File
@@ -1,8 +1,9 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals' import {beforeAll, describe, it} from '@jest/globals'
import {expect} from 'detox' import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
import {createServer, loginAsAlice, openApp} from '../util'
describe('Home screen', () => { describe('Home screen', () => {
beforeAll(async () => { beforeAll(async () => {
@@ -68,19 +69,16 @@ describe('Home screen', () => {
).not.toExist() ).not.toExist()
}) })
it('Can report posts', async () => { // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
const carlaPosts = by.id('feedItem-by-carla.test') // it('Can report posts', async () => {
await element(by.id('postDropdownBtn').withAncestor(carlaPosts)) // const carlaPosts = by.id('feedItem-by-carla.test')
.atIndex(0) // await element(by.id('postDropdownBtn').withAncestor(carlaPosts))
.tap() // .atIndex(0)
await element(by.text('Report post')).tap() // .tap()
await expect(element(by.id('reportModal'))).toBeVisible() // await element(by.text('Report post')).tap()
await element( // await element(by.id('com.atproto.moderation.defs#reasonSpam')).tap()
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), // await element(by.id('sendReportBtn')).tap()
).tap() // })
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
it('Can swipe between feeds', async () => { it('Can swipe between feeds', async () => {
await element(by.id('homeScreen')).swipe('left', 'fast', 0.75) await element(by.id('homeScreen')).swipe('left', 'fast', 0.75)
@@ -1,8 +1,9 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals' import {beforeAll, describe, it} from '@jest/globals'
import {expect} from 'detox' import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
import {createServer, loginAsAlice, openApp} from '../util'
describe('invite-codes', () => { describe('invite-codes', () => {
let service: string let service: string
@@ -1,52 +0,0 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
describe('invite-codes', () => {
let service: string
let inviteCode = ''
beforeAll(async () => {
service = await createServer('?users&invite&phone')
await openApp({permissions: {notifications: 'YES'}})
})
it('I can fetch invite codes', async () => {
await loginAsAlice()
await element(by.id('e2eOpenInviteCodesModal')).tap()
await expect(element(by.id('inviteCodesModal'))).toBeVisible()
const attrs = await element(by.id('inviteCode-0-code')).getAttributes()
inviteCode = attrs.text
await element(by.id('closeBtn')).tap()
await element(by.id('e2eSignOut')).tap()
})
it('I can create a new account with the invite code', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server')
await element(by.id('customSelectBtn')).tap()
await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('doneBtn')).tap()
await device.takeScreenshot('3- input test server URL')
await element(by.id('inviteCodeInput')).typeText(inviteCode)
await element(by.id('emailInput')).typeText('example@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('2345551234')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('000000')
await device.takeScreenshot('6- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('e2e-test')
await device.takeScreenshot('7- entered handle')
await element(by.id('nextBtn')).tap()
await expect(element(by.id('onboardingInterests'))).toBeVisible()
})
})
+26 -23
View File
@@ -1,8 +1,9 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals' import {beforeAll, describe, it} from '@jest/globals'
import {expect} from 'detox' import {expect} from 'detox'
import {openApp, loginAsAlice, createServer, sleep} from '../util'
import {createServer, loginAsAlice, openApp, sleep} from '../util'
describe('Profile screen', () => { describe('Profile screen', () => {
beforeAll(async () => { beforeAll(async () => {
@@ -124,16 +125,17 @@ describe('Profile screen', () => {
await expect(element(by.id('profileHeaderAlert'))).not.toExist() await expect(element(by.id('profileHeaderAlert'))).not.toExist()
}) })
it('Can report another user', async () => { // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
await element(by.id('profileHeaderDropdownBtn')).tap() // it('Can report another user', async () => {
await element(by.text('Report Account')).tap() // await element(by.id('profileHeaderDropdownBtn')).tap()
await expect(element(by.id('reportModal'))).toBeVisible() // await element(by.text('Report Account')).tap()
await element( // await expect(element(by.id('reportModal'))).toBeVisible()
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), // await element(
).tap() // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
await element(by.id('sendReportBtn')).tap() // ).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible() // await element(by.id('sendReportBtn')).tap()
}) // await expect(element(by.id('reportModal'))).not.toBeVisible()
// })
it('Can like posts', async () => { it('Can like posts', async () => {
await element(by.id('postsFeed-flatlist')).swipe( await element(by.id('postsFeed-flatlist')).swipe(
@@ -179,15 +181,16 @@ describe('Profile screen', () => {
).not.toExist() ).not.toExist()
}) })
it('Can report posts', async () => { // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
const posts = by.id('feedItem-by-bob.test') // it('Can report posts', async () => {
await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap() // const posts = by.id('feedItem-by-bob.test')
await element(by.text('Report post')).tap() // await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
await expect(element(by.id('reportModal'))).toBeVisible() // await element(by.text('Report post')).tap()
await element( // await expect(element(by.id('reportModal'))).toBeVisible()
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), // await element(
).tap() // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
await element(by.id('sendReportBtn')).tap() // ).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible() // await element(by.id('sendReportBtn')).tap()
}) // await expect(element(by.id('reportModal'))).not.toBeVisible()
// })
}) })
@@ -1,83 +0,0 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, createServer} from '../util'
describe('Create account', () => {
let service: string
beforeAll(async () => {
service = await createServer('?phone')
await openApp({permissions: {notifications: 'YES'}})
})
it('I can create a new account with text verification', async () => {
console.log('SERVICE IS', service)
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server')
await element(by.id('customSelectBtn')).tap()
await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('doneBtn')).tap()
await device.takeScreenshot('3- input test server URL')
await element(by.id('emailInput')).typeText('text-verification@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('text-verification-test')
await device.takeScreenshot('5- entered handle')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('8042221111')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('6- requested code')
await element(by.id('codeInput')).typeText('000000')
await device.takeScreenshot('7- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('nextBtn')).tap()
await expect(element(by.id('onboardingInterests'))).toBeVisible()
})
it('failed text verification correctly goes back to the code input screen', async () => {
await element(by.id('e2eSignOut')).tap()
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('selectServiceButton')).tap()
await device.takeScreenshot('2- selected other server')
await element(by.id('customSelectBtn')).tap()
await element(by.id('customServerTextInput')).typeText(service)
await element(by.id('customServerTextInput')).tapReturnKey()
await element(by.id('doneBtn')).tap()
await device.takeScreenshot('3- input test server URL')
await element(by.id('emailInput')).typeText('text-verification2@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('8042221111')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('111111')
await device.takeScreenshot('6- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('text-verification-test2')
await device.takeScreenshot('7- entered handle')
await element(by.id('nextBtn')).tap()
await expect(element(by.id('codeInput'))).toBeVisible()
await device.takeScreenshot('8- got error')
})
})
+27 -24
View File
@@ -1,8 +1,9 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals' import {beforeAll, describe, it} from '@jest/globals'
import {expect} from 'detox' import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
import {createServer, loginAsAlice, openApp} from '../util'
describe('Thread screen', () => { describe('Thread screen', () => {
beforeAll(async () => { beforeAll(async () => {
@@ -102,27 +103,29 @@ describe('Thread screen', () => {
).not.toExist() ).not.toExist()
}) })
it('Can report the root post', async () => { // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
const post = by.id('postThreadItem-by-bob.test') // it('Can report the root post', async () => {
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() // const post = by.id('postThreadItem-by-bob.test')
await element(by.text('Report post')).tap() // await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
await expect(element(by.id('reportModal'))).toBeVisible() // await element(by.text('Report post')).tap()
await element( // await expect(element(by.id('reportModal'))).toBeVisible()
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), // await element(
).tap() // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
await element(by.id('sendReportBtn')).tap() // ).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible() // await element(by.id('sendReportBtn')).tap()
}) // await expect(element(by.id('reportModal'))).not.toBeVisible()
// })
it('Can report a reply post', async () => { // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
const post = by.id('postThreadItem-by-carla.test') // it('Can report a reply post', async () => {
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() // const post = by.id('postThreadItem-by-carla.test')
await element(by.text('Report post')).tap() // await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
await expect(element(by.id('reportModal'))).toBeVisible() // await element(by.text('Report post')).tap()
await element( // await expect(element(by.id('reportModal'))).toBeVisible()
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), // await element(
).tap() // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
await element(by.id('sendReportBtn')).tap() // ).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible() // await element(by.id('sendReportBtn')).tap()
}) // await expect(element(by.id('reportModal'))).not.toBeVisible()
// })
}) })
+59 -96
View File
@@ -1,8 +1,8 @@
import {AtUri, BskyAgent} from '@atproto/api'
import {TestBsky, TestNetwork} from '@atproto/dev-env'
import fs from 'fs'
import net from 'net' import net from 'net'
import path from 'path' import path from 'path'
import fs from 'fs'
import {TestNetwork, TestPds} from '@atproto/dev-env'
import {AtUri, BskyAgent} from '@atproto/api'
export interface TestUser { export interface TestUser {
email: string email: string
@@ -55,12 +55,8 @@ class StringIdGenerator {
const ids = new StringIdGenerator() const ids = new StringIdGenerator()
export async function createServer( export async function createServer(
{ {inviteRequired}: {inviteRequired: boolean} = {
inviteRequired,
phoneRequired,
}: {inviteRequired: boolean; phoneRequired: boolean} = {
inviteRequired: false, inviteRequired: false,
phoneRequired: false,
}, },
): Promise<TestPDS> { ): Promise<TestPDS> {
const port = 3000 const port = 3000
@@ -69,23 +65,11 @@ export async function createServer(
const pdsUrl = `http://localhost:${port}` const pdsUrl = `http://localhost:${port}`
const id = ids.next() const id = ids.next()
const phoneParams = phoneRequired
? {
phoneVerificationRequired: true,
phoneVerificationProvider: 'twilio',
twilioAccountSid: 'ACXXXXXXX',
twilioAuthToken: 'AUTH',
twilioServiceSid: 'VAXXXXXXXX',
}
: {}
const testNet = await TestNetwork.create({ const testNet = await TestNetwork.create({
pds: { pds: {
port, port,
hostname: 'localhost', hostname: 'localhost',
dbPostgresSchema: `pds_${id}`,
inviteRequired, inviteRequired,
...phoneParams,
}, },
bsky: { bsky: {
dbPostgresSchema: `bsky_${id}`, dbPostgresSchema: `bsky_${id}`,
@@ -94,36 +78,33 @@ export async function createServer(
}, },
plc: {port: port2}, plc: {port: port2},
}) })
mockTwilio(testNet.pds)
// add the test mod authority // add the test mod authority
if (!phoneRequired) { const agent = new BskyAgent({service: pdsUrl})
const agent = new BskyAgent({service: pdsUrl}) const res = await agent.api.com.atproto.server.createAccount({
const res = await agent.api.com.atproto.server.createAccount({ email: 'mod-authority@test.com',
email: 'mod-authority@test.com', handle: 'mod-authority.test',
handle: 'mod-authority.test', password: 'hunter2',
password: 'hunter2', })
}) agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`) await agent.api.app.bsky.actor.profile.create(
await agent.api.app.bsky.actor.profile.create( {repo: res.data.did},
{repo: res.data.did}, {
{ displayName: 'Dev-env Moderation',
displayName: 'Dev-env Moderation', description: `The pretend version of mod.bsky.app`,
description: `The pretend version of mod.bsky.app`, },
}, )
)
await agent.api.app.bsky.labeler.service.create( await agent.api.app.bsky.labeler.service.create(
{repo: res.data.did, rkey: 'self'}, {repo: res.data.did, rkey: 'self'},
{ {
policies: { policies: {
labelValues: ['!hide', '!warn'], labelValues: ['!hide', '!warn'],
labelValueDefinitions: [], labelValueDefinitions: [],
},
createdAt: new Date().toISOString(),
}, },
) createdAt: new Date().toISOString(),
} },
)
const pic = fs.readFileSync( const pic = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'default-avatar.png'), path.join(__dirname, '..', 'assets', 'default-avatar.png'),
@@ -181,7 +162,7 @@ class Mocker {
const inviteRes = await agent.api.com.atproto.server.createInviteCode( const inviteRes = await agent.api.com.atproto.server.createInviteCode(
{useCount: 1}, {useCount: 1},
{ {
headers: this.pds.adminAuthHeaders('admin'), headers: this.pds.adminAuthHeaders(),
encoding: 'application/json', encoding: 'application/json',
}, },
) )
@@ -192,8 +173,6 @@ class Mocker {
email, email,
handle: name + '.test', handle: name + '.test',
password: 'hunter2', password: 'hunter2',
verificationPhone: '1234567890',
verificationCode: '000000',
}) })
await agent.upsertProfile(async () => { await agent.upsertProfile(async () => {
const blob = await agent.uploadBlob(this.pic, { const blob = await agent.uploadBlob(this.pic, {
@@ -358,7 +337,7 @@ class Mocker {
await agent.api.com.atproto.server.createInviteCode( await agent.api.com.atproto.server.createInviteCode(
{useCount: 1, forAccount}, {useCount: 1, forAccount},
{ {
headers: this.pds.adminAuthHeaders('admin'), headers: this.pds.adminAuthHeaders(),
encoding: 'application/json', encoding: 'application/json',
}, },
) )
@@ -373,18 +352,11 @@ class Mocker {
if (!ctx) { if (!ctx) {
throw new Error('Invalid appview') throw new Error('Invalid appview')
} }
const labelSrvc = ctx.services.label(ctx.db.getPrimary()) await createLabel(this.bsky, {
await labelSrvc.createLabels([ uri: did,
{ cid: '',
// @ts-ignore val: label,
src: ctx.cfg.labelerDid, })
uri: did,
cid: '',
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
} }
async labelProfile(label: string, user: string) { async labelProfile(label: string, user: string) {
@@ -403,18 +375,11 @@ class Mocker {
if (!ctx) { if (!ctx) {
throw new Error('Invalid appview') throw new Error('Invalid appview')
} }
const labelSrvc = ctx.services.label(ctx.db.getPrimary()) await createLabel(this.bsky, {
await labelSrvc.createLabels([ uri: profile.uri,
{ cid: profile.cid,
// @ts-ignore val: label,
src: ctx.cfg.labelerDid, })
uri: profile.uri,
cid: profile.cid,
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
} }
async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) { async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) {
@@ -422,18 +387,11 @@ class Mocker {
if (!ctx) { if (!ctx) {
throw new Error('Invalid appview') throw new Error('Invalid appview')
} }
const labelSrvc = ctx.services.label(ctx.db.getPrimary()) await createLabel(this.bsky, {
await labelSrvc.createLabels([ uri,
{ cid,
// @ts-ignore val: label,
src: ctx.cfg.labelerDid, })
uri,
cid,
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
} }
async createMuteList(user: string, name: string): Promise<string> { async createMuteList(user: string, name: string): Promise<string> {
@@ -484,14 +442,19 @@ async function getPort(start = 3000) {
throw new Error('Unable to find an available port') throw new Error('Unable to find an available port')
} }
export const mockTwilio = (pds: TestPds) => { const createLabel = async (
if (!pds.ctx.phoneVerifier) return bsky: TestBsky,
opts: {uri: string; cid: string; val: string},
pds.ctx.phoneVerifier.sendCode = async (_number: string) => { ) => {
// do nothing await bsky.db.db
} .insertInto('label')
.values({
pds.ctx.phoneVerifier.verifyCode = async (_number: string, code: string) => { uri: opts.uri,
return code === '000000' cid: opts.cid,
} val: opts.val,
cts: new Date().toISOString(),
neg: false,
src: 'did:example:labeler',
})
.execute()
} }
+1 -1
View File
@@ -192,7 +192,7 @@
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@atproto/dev-env": "^0.2.28", "@atproto/dev-env": "^0.3.4",
"@babel/core": "^7.23.2", "@babel/core": "^7.23.2",
"@babel/preset-env": "^7.20.0", "@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",
+6 -1
View File
@@ -539,8 +539,10 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) const theme = useColorSchemeStyle(DefaultTheme, DarkTheme)
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const prevLoggedRouteName = React.useRef<string | undefined>(undefined)
function onReady() { function onReady() {
prevLoggedRouteName.current = getCurrentRouteName()
initAnalytics(currentAccount) initAnalytics(currentAccount)
if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) { if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) {
@@ -555,7 +557,10 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
linking={LINKING} linking={LINKING}
theme={theme} theme={theme}
onStateChange={() => { onStateChange={() => {
logEvent('router:navigate', {}) logEvent('router:navigate', {
from: prevLoggedRouteName.current,
})
prevLoggedRouteName.current = getCurrentRouteName()
}} }}
onReady={() => { onReady={() => {
attachRouteToLogEvents(getCurrentRouteName) attachRouteToLogEvents(getCurrentRouteName)
+1 -2
View File
@@ -39,8 +39,7 @@ export function useDialogControl(): DialogOuterProps['control'] {
control.current.open() control.current.open()
}, },
close: cb => { close: cb => {
control.current.close() control.current.close(cb)
cb?.()
}, },
}), }),
[id, control], [id, control],
+34 -20
View File
@@ -83,7 +83,7 @@ export function Outer({
const sheetOptions = nativeOptions?.sheet || {} const sheetOptions = nativeOptions?.sheet || {}
const hasSnapPoints = !!sheetOptions.snapPoints const hasSnapPoints = !!sheetOptions.snapPoints
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const closeCallback = React.useRef<() => void>() const closeCallbacks = React.useRef<(() => void)[]>([])
const {setDialogIsOpen} = useDialogStateControlContext() const {setDialogIsOpen} = useDialogStateControlContext()
/* /*
@@ -96,22 +96,51 @@ export function Outer({
*/ */
const isOpen = openIndex > -1 const isOpen = openIndex > -1
const callQueuedCallbacks = React.useCallback(() => {
for (const cb of closeCallbacks.current) {
try {
cb()
} catch (e: any) {
logger.error('Error running close callback', e)
}
}
closeCallbacks.current = []
}, [])
const open = React.useCallback<DialogControlProps['open']>( const open = React.useCallback<DialogControlProps['open']>(
({index} = {}) => { ({index} = {}) => {
// Run any leftover callbacks that might have been queued up before calling `.open()`
callQueuedCallbacks()
setDialogIsOpen(control.id, true) setDialogIsOpen(control.id, true)
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open" // can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
setOpenIndex(index || 0) setOpenIndex(index || 0)
sheet.current?.snapToIndex(index || 0)
}, },
[setOpenIndex, setDialogIsOpen, control.id], [setDialogIsOpen, control.id, callQueuedCallbacks],
) )
// This is the function that we call when we want to dismiss the dialog.
const close = React.useCallback<DialogControlProps['close']>(cb => { const close = React.useCallback<DialogControlProps['close']>(cb => {
if (cb && typeof cb === 'function') { if (typeof cb === 'function') {
closeCallback.current = cb closeCallbacks.current.push(cb)
} }
sheet.current?.close() sheet.current?.close()
}, []) }, [])
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
// happen before we run this. It is passed to the `BottomSheet` component.
const onCloseAnimationComplete = React.useCallback(() => {
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
// tells us that we need to toggle the accessibility overlay setting
setDialogIsOpen(control.id, false)
setOpenIndex(-1)
callQueuedCallbacks()
onClose?.()
}, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen])
useImperativeHandle( useImperativeHandle(
control.ref, control.ref,
() => ({ () => ({
@@ -121,21 +150,6 @@ export function Outer({
[open, close], [open, close],
) )
const onCloseInner = React.useCallback(() => {
try {
closeCallback.current?.()
} catch (e: any) {
logger.error(`Dialog closeCallback failed`, {
message: e.message,
})
} finally {
closeCallback.current = undefined
}
setDialogIsOpen(control.id, false)
onClose?.()
setOpenIndex(-1)
}, [control.id, onClose, setDialogIsOpen])
const context = React.useMemo(() => ({close}), [close]) const context = React.useMemo(() => ({close}), [close])
return ( return (
@@ -163,7 +177,7 @@ export function Outer({
backdropComponent={Backdrop} backdropComponent={Backdrop}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}} handleStyle={{display: 'none'}}
onClose={onCloseInner}> onClose={onCloseAnimationComplete}>
<Context.Provider value={context}> <Context.Provider value={context}>
<View <View
style={[ style={[
+27 -28
View File
@@ -33,40 +33,41 @@ export function Outer({
const t = useTheme() const t = useTheme()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const [isOpen, setIsOpen] = React.useState(false) const [isOpen, setIsOpen] = React.useState(false)
const [isVisible, setIsVisible] = React.useState(true)
const {setDialogIsOpen} = useDialogStateControlContext() const {setDialogIsOpen} = useDialogStateControlContext()
const open = React.useCallback(() => { const open = React.useCallback(() => {
setIsOpen(true)
setDialogIsOpen(control.id, true) setDialogIsOpen(control.id, true)
setIsOpen(true)
}, [setIsOpen, setDialogIsOpen, control.id]) }, [setIsOpen, setDialogIsOpen, control.id])
const onCloseInner = React.useCallback(async () => {
setIsVisible(false)
await new Promise(resolve => setTimeout(resolve, 150))
setIsOpen(false)
setIsVisible(true)
setDialogIsOpen(control.id, false)
onClose?.()
}, [control.id, onClose, setDialogIsOpen])
const close = React.useCallback<DialogControlProps['close']>( const close = React.useCallback<DialogControlProps['close']>(
cb => { cb => {
setDialogIsOpen(control.id, false)
setIsOpen(false)
try { try {
if (cb && typeof cb === 'function') { if (cb && typeof cb === 'function') {
cb() // This timeout ensures that the callback runs at the same time as it would on native. I.e.
// console.log('Step 1') -> close(() => console.log('Step 3')) -> console.log('Step 2')
// This should always output 'Step 1', 'Step 2', 'Step 3', but without the timeout it would output
// 'Step 1', 'Step 3', 'Step 2'.
setTimeout(cb)
} }
} catch (e: any) { } catch (e: any) {
logger.error(`Dialog closeCallback failed`, { logger.error(`Dialog closeCallback failed`, {
message: e.message, message: e.message,
}) })
} finally {
onCloseInner()
} }
onClose?.()
}, },
[onCloseInner], [control.id, onClose, setDialogIsOpen],
) )
const handleBackgroundPress = React.useCallback(async () => {
close()
}, [close])
useImperativeHandle( useImperativeHandle(
control.ref, control.ref,
() => ({ () => ({
@@ -103,7 +104,7 @@ export function Outer({
<TouchableWithoutFeedback <TouchableWithoutFeedback
accessibilityHint={undefined} accessibilityHint={undefined}
accessibilityLabel={_(msg`Close active dialog`)} accessibilityLabel={_(msg`Close active dialog`)}
onPress={onCloseInner}> onPress={handleBackgroundPress}>
<View <View
style={[ style={[
web(a.fixed), web(a.fixed),
@@ -113,17 +114,15 @@ export function Outer({
gtMobile ? a.p_lg : a.p_md, gtMobile ? a.p_lg : a.p_md,
{overflowY: 'auto'}, {overflowY: 'auto'},
]}> ]}>
{isVisible && ( <Animated.View
<Animated.View entering={FadeIn.duration(150)}
entering={FadeIn.duration(150)} // exiting={FadeOut.duration(150)}
// exiting={FadeOut.duration(150)} style={[
style={[ web(a.fixed),
web(a.fixed), a.inset_0,
a.inset_0, {opacity: 0.8, backgroundColor: t.palette.black},
{opacity: 0.8, backgroundColor: t.palette.black}, ]}
]} />
/>
)}
<View <View
style={[ style={[
@@ -135,7 +134,7 @@ export function Outer({
minHeight: web('calc(90vh - 36px)') || undefined, minHeight: web('calc(90vh - 36px)') || undefined,
}, },
]}> ]}>
{isVisible ? children : null} {children}
</View> </View>
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
+14
View File
@@ -123,6 +123,13 @@ export function Action({
cta, cta,
testID, testID,
}: { }: {
/**
* Callback to run when the action is pressed. The method is called _after_
* the dialog closes.
*
* Note: The dialog will close automatically when the action is pressed, you
* should NOT close the dialog as a side effect of this method.
*/
onPress: () => void onPress: () => void
color?: ButtonColor color?: ButtonColor
/** /**
@@ -165,6 +172,13 @@ export function Basic({
description: string description: string
cancelButtonCta?: string cancelButtonCta?: string
confirmButtonCta?: string confirmButtonCta?: string
/**
* Callback to run when the Confirm button is pressed. The method is called
* _after_ the dialog closes.
*
* Note: The dialog will close automatically when the action is pressed, you
* should NOT close the dialog as a side effect of this method.
*/
onConfirm: () => void onConfirm: () => void
confirmButtonColor?: ButtonColor confirmButtonColor?: ButtonColor
}>) { }>) {
@@ -90,6 +90,7 @@ export function SelectReportOptionView({
return ( return (
<Button <Button
key={reportOption.reason} key={reportOption.reason}
testID={reportOption.reason}
label={_(msg`Create report for ${reportOption.title}`)} label={_(msg`Create report for ${reportOption.title}`)}
onPress={() => props.onSelectReportOption(reportOption)}> onPress={() => props.onSelectReportOption(reportOption)}>
<ReportOptionButton <ReportOptionButton
+13 -14
View File
@@ -1,25 +1,23 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
import {getLabelingServiceTitle} from '#/lib/moderation' import {getLabelingServiceTitle} from '#/lib/moderation'
import {ReportOption} from '#/lib/moderation/useReportOptions' import {ReportOption} from '#/lib/moderation/useReportOptions'
import {atoms as a, useTheme, native} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import * as Toggle from '#/components/forms/Toggle'
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
import {Loader} from '#/components/Loader'
import * as Toast from '#/view/com/util/Toast'
import {ReportDialogProps} from './types'
import {getAgent} from '#/state/session' import {getAgent} 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'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
export function SubmitView({ export function SubmitView({
params, params,
@@ -208,6 +206,7 @@ export function SubmitView({
))} ))}
<Button <Button
testID="sendReportBtn"
size="large" size="large"
variant="solid" variant="solid"
color="negative" color="negative"
+119
View File
@@ -0,0 +1,119 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
type EmbedPlayerSource,
embedPlayerSources,
externalEmbedLabels,
} from '#/lib/strings/embed-player'
import {useSetExternalEmbedPref} from '#/state/preferences'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Button, ButtonText} from '../Button'
import {Text} from '../Typography'
export function EmbedConsentDialog({
control,
source,
onAccept,
}: {
control: Dialog.DialogControlProps
source: EmbedPlayerSource
onAccept: () => void
}) {
const {_} = useLingui()
const t = useTheme()
const setExternalEmbedPref = useSetExternalEmbedPref()
const {gtMobile} = useBreakpoints()
const onShowAllPress = useCallback(() => {
for (const key of embedPlayerSources) {
setExternalEmbedPref(key, 'show')
}
onAccept()
control.close()
}, [control, onAccept, setExternalEmbedPref])
const onShowPress = useCallback(() => {
setExternalEmbedPref(source, 'show')
onAccept()
control.close()
}, [control, onAccept, setExternalEmbedPref, source])
const onHidePress = useCallback(() => {
setExternalEmbedPref(source, 'hide')
control.close()
}, [control, setExternalEmbedPref, source])
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`External Media`)}
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
<View style={a.gap_sm}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>External Media</Trans>
</Text>
<View style={[a.mt_sm, a.mb_2xl, a.gap_lg]}>
<Text>
<Trans>
This content is hosted by {externalEmbedLabels[source]}. Do you
want to enable external media?
</Trans>
</Text>
<Text style={t.atoms.text_contrast_medium}>
<Trans>
External media may allow websites to collect information about
you and your device. No information is sent or requested until
you press the "play" button.
</Trans>
</Text>
</View>
</View>
<View style={a.gap_md}>
<Button
style={gtMobile && a.flex_1}
label={_(msg`Enable external media`)}
onPress={onShowAllPress}
onAccessibilityEscape={control.close}
color="primary"
size="medium"
variant="solid">
<ButtonText>
<Trans>Enable external media</Trans>
</ButtonText>
</Button>
<Button
style={gtMobile && a.flex_1}
label={_(msg`Enable this source only`)}
onPress={onShowPress}
onAccessibilityEscape={control.close}
color="secondary"
size="medium"
variant="solid">
<ButtonText>
<Trans>Enable {externalEmbedLabels[source]} only</Trans>
</ButtonText>
</Button>
<Button
label={_(msg`No thanks`)}
onAccessibilityEscape={control.close}
onPress={onHidePress}
color="secondary"
size="medium"
variant="ghost">
<ButtonText>
<Trans>No thanks</Trans>
</ButtonText>
</Button>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+1
View File
@@ -41,6 +41,7 @@ export function HostingProvider({
onSelect={onSelectServiceUrl} onSelect={onSelectServiceUrl}
/> />
<Button <Button
testID="loginSelectServiceButton"
label={toNiceDomain(serviceUrl)} label={toNiceDomain(serviceUrl)}
accessibilityHint={_(msg`Press to change hosting provider`)} accessibilityHint={_(msg`Press to change hosting provider`)}
variant="solid" variant="solid"
+3 -1
View File
@@ -80,8 +80,10 @@ export const HITSLOP_30 = createHitslop(30)
export const BACK_HITSLOP = HITSLOP_30 export const BACK_HITSLOP = HITSLOP_30
export const MAX_POST_LINES = 25 export const MAX_POST_LINES = 25
export const BSKY_APP_ACCOUNT_DID = 'did:plc:z72i7hdynmk6r22z27h6tvur'
export const BSKY_FEED_OWNER_DIDS = [ export const BSKY_FEED_OWNER_DIDS = [
'did:plc:z72i7hdynmk6r22z27h6tvur', BSKY_APP_ACCOUNT_DID,
'did:plc:vpkhqolt662uhesyj6nxm7ys', 'did:plc:vpkhqolt662uhesyj6nxm7ys',
'did:plc:q6gjnaw2blty4crticxkmujt', 'did:plc:q6gjnaw2blty4crticxkmujt',
] ]
+6
View File
@@ -44,6 +44,12 @@ export type LogEvents = {
} }
'onboarding:moderation:nextPressed': {} 'onboarding:moderation:nextPressed': {}
'onboarding:finished:nextPressed': {} 'onboarding:finished:nextPressed': {}
'home:feedDisplayed': {
feedUrl: string
feedType: string
index: number
reason: 'focus' | 'tabbar-click' | 'pager-swipe' | 'desktop-sidebar-click'
}
'feed:endReached': { 'feed:endReached': {
feedUrl: string feedUrl: string
feedType: string feedType: string
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -34,11 +34,6 @@ export const PRIMARY_FEEDS: FeedConfig[] = [
uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot', uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot',
gradient: tokens.gradients.midnight, gradient: tokens.gradients.midnight,
}, },
{
default: IS_PROD, // these feeds are only available in prod
uri: 'at://did:plc:wqowuobffl66jv3kpsvo7ak4/app.bsky.feed.generator/the-algorithm',
gradient: tokens.gradients.midnight,
},
] ]
const SECONDARY_FEEDS: FeedConfig[] = [ const SECONDARY_FEEDS: FeedConfig[] = [
@@ -130,16 +125,6 @@ export function StepAlgoFeeds() {
<Trans>We recommend our "Discover" feed:</Trans> <Trans>We recommend our "Discover" feed:</Trans>
</Text> </Text>
<FeedCard config={PRIMARY_FEEDS[0]} /> <FeedCard config={PRIMARY_FEEDS[0]} />
<Text
style={[
a.text_md,
a.pt_4xl,
a.pb_lg,
t.atoms.text_contrast_medium,
]}>
<Trans>We also think you'll like "For You" by Skygaze:</Trans>
</Text>
<FeedCard config={PRIMARY_FEEDS[1]} />
</Toggle.Group> </Toggle.Group>
<Toggle.Group <Toggle.Group
+4 -1
View File
@@ -4,6 +4,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {BSKY_APP_ACCOUNT_DID} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useSetSaveFeedsMutation} from '#/state/queries/preferences' import {useSetSaveFeedsMutation} from '#/state/queries/preferences'
@@ -55,7 +56,9 @@ export function StepFinished() {
try { try {
await Promise.all([ await Promise.all([
bulkWriteFollows(suggestedAccountsStepResults.accountDids), bulkWriteFollows(
suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID),
),
// these must be serial // these must be serial
(async () => { (async () => {
await getAgent().setInterestsPref({tags: selectedInterests}) await getAgent().setInterestsPref({tags: selectedInterests})
-8
View File
@@ -3,7 +3,6 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {EmbedPlayerSource} from '#/lib/strings/embed-player'
import {GalleryModel} from '#/state/models/media/gallery' import {GalleryModel} from '#/state/models/media/gallery'
import {ImageModel} from '#/state/models/media/image' import {ImageModel} from '#/state/models/media/image'
import {ThreadgateSetting} from '../queries/threadgate' import {ThreadgateSetting} from '../queries/threadgate'
@@ -125,12 +124,6 @@ export interface LinkWarningModal {
share?: boolean share?: boolean
} }
export interface EmbedConsentModal {
name: 'embed-consent'
source: EmbedPlayerSource
onAccept: () => void
}
export interface InAppBrowserConsentModal { export interface InAppBrowserConsentModal {
name: 'in-app-browser-consent' name: 'in-app-browser-consent'
href: string href: string
@@ -169,7 +162,6 @@ export type Modal =
// Generic // Generic
| LinkWarningModal | LinkWarningModal
| EmbedConsentModal
| InAppBrowserConsentModal | InAppBrowserConsentModal
const ModalContext = React.createContext<{ const ModalContext = React.createContext<{
+1 -3
View File
@@ -507,9 +507,7 @@ export const ComposePost = observer(function ComposePost({
control={discardPromptControl} control={discardPromptControl}
title={_(msg`Discard draft?`)} title={_(msg`Discard draft?`)}
description={_(msg`Are you sure you'd like to discard this draft?`)} description={_(msg`Are you sure you'd like to discard this draft?`)}
onConfirm={() => { onConfirm={onClose}
discardPromptControl.close(onClose)
}}
confirmButtonCta={_(msg`Discard`)} confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative" confirmButtonColor="negative"
/> />
-154
View File
@@ -1,154 +0,0 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {
EmbedPlayerSource,
embedPlayerSources,
externalEmbedLabels,
} from '#/lib/strings/embed-player'
import {useModalControls} from '#/state/modals'
import {useSetExternalEmbedPref} from '#/state/preferences/external-embeds-prefs'
import {usePalette} from 'lib/hooks/usePalette'
import {colors, gradients, s} from 'lib/styles'
import {Text} from '../util/text/Text'
import {ScrollView} from './util'
export const snapPoints = [450]
export function Component({
onAccept,
source,
}: {
onAccept: () => void
source: EmbedPlayerSource
}) {
const pal = usePalette('default')
const {closeModal} = useModalControls()
const {_} = useLingui()
const setExternalEmbedPref = useSetExternalEmbedPref()
const {isMobile} = useWebMediaQueries()
const onShowAllPress = React.useCallback(() => {
for (const key of embedPlayerSources) {
setExternalEmbedPref(key, 'show')
}
onAccept()
closeModal()
}, [closeModal, onAccept, setExternalEmbedPref])
const onShowPress = React.useCallback(() => {
setExternalEmbedPref(source, 'show')
onAccept()
closeModal()
}, [closeModal, onAccept, setExternalEmbedPref, source])
const onHidePress = React.useCallback(() => {
setExternalEmbedPref(source, 'hide')
closeModal()
}, [closeModal, setExternalEmbedPref, source])
return (
<ScrollView
testID="embedConsentModal"
style={[
s.flex1,
pal.view,
isMobile
? {paddingHorizontal: 20, paddingTop: 10}
: {paddingHorizontal: 30},
]}>
<Text style={[pal.text, styles.title]}>
<Trans>External Media</Trans>
</Text>
<Text style={pal.text}>
<Trans>
This content is hosted by {externalEmbedLabels[source]}. Do you want
to enable external media?
</Trans>
</Text>
<View style={[s.mt10]} />
<Text style={pal.textLight}>
<Trans>
External media may allow websites to collect information about you and
your device. No information is sent or requested until you press the
"play" button.
</Trans>
</Text>
<View style={[s.mt20]} />
<TouchableOpacity
testID="enableAllBtn"
onPress={onShowAllPress}
accessibilityRole="button"
accessibilityLabel={_(
msg`Show embeds from ${externalEmbedLabels[source]}`,
)}
accessibilityHint=""
onAccessibilityEscape={closeModal}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Enable External Media</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
<View style={[s.mt10]} />
<TouchableOpacity
testID="enableSourceBtn"
onPress={onShowPress}
accessibilityRole="button"
accessibilityLabel={_(
msg`Never load embeds from ${externalEmbedLabels[source]}`,
)}
accessibilityHint=""
onAccessibilityEscape={closeModal}>
<View style={[styles.btn, pal.btn]}>
<Text style={[pal.text, s.bold, s.f18]}>
<Trans>Enable {externalEmbedLabels[source]} only</Trans>
</Text>
</View>
</TouchableOpacity>
<View style={[s.mt10]} />
<TouchableOpacity
testID="disableSourceBtn"
onPress={onHidePress}
accessibilityRole="button"
accessibilityLabel={_(
msg`Never load embeds from ${externalEmbedLabels[source]}`,
)}
accessibilityHint=""
onAccessibilityEscape={closeModal}>
<View style={[styles.btn, pal.btn]}>
<Text style={[pal.text, s.bold, s.f18]}>
<Trans>No thanks</Trans>
</Text>
</View>
</TouchableOpacity>
</ScrollView>
)
}
const styles = StyleSheet.create({
title: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 24,
marginBottom: 12,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
borderRadius: 32,
padding: 14,
backgroundColor: colors.gray1,
},
})
-4
View File
@@ -15,7 +15,6 @@ import * as ChangePasswordModal from './ChangePassword'
import * as CreateOrEditListModal from './CreateOrEditList' import * as CreateOrEditListModal from './CreateOrEditList'
import * as DeleteAccountModal from './DeleteAccount' import * as DeleteAccountModal from './DeleteAccount'
import * as EditProfileModal from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as EmbedConsentModal from './EmbedConsent'
import * as InAppBrowserConsentModal from './InAppBrowserConsent' import * as InAppBrowserConsentModal from './InAppBrowserConsent'
import * as InviteCodesModal from './InviteCodes' import * as InviteCodesModal from './InviteCodes'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
@@ -116,9 +115,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'link-warning') { } else if (activeModal?.name === 'link-warning') {
snapPoints = LinkWarningModal.snapPoints snapPoints = LinkWarningModal.snapPoints
element = <LinkWarningModal.Component {...activeModal} /> element = <LinkWarningModal.Component {...activeModal} />
} else if (activeModal?.name === 'embed-consent') {
snapPoints = EmbedConsentModal.snapPoints
element = <EmbedConsentModal.Component {...activeModal} />
} else if (activeModal?.name === 'in-app-browser-consent') { } else if (activeModal?.name === 'in-app-browser-consent') {
snapPoints = InAppBrowserConsentModal.snapPoints snapPoints = InAppBrowserConsentModal.snapPoints
element = <InAppBrowserConsentModal.Component {...activeModal} /> element = <InAppBrowserConsentModal.Component {...activeModal} />
+19 -22
View File
@@ -1,33 +1,32 @@
import React from 'react' import React from 'react'
import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native' import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import type {Modal as ModalIface} from '#/state/modals'
import {useModalControls, useModals} from '#/state/modals'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' import * as AddAppPassword from './AddAppPasswords'
import * as AltTextImageModal from './AltImage'
import {useModals, useModalControls} from '#/state/modals' import * as ChangeEmailModal from './ChangeEmail'
import type {Modal as ModalIface} from '#/state/modals' import * as ChangeHandleModal from './ChangeHandle'
import * as EditProfileModal from './EditProfile' import * as ChangePasswordModal from './ChangePassword'
import * as CreateOrEditListModal from './CreateOrEditList' import * as CreateOrEditListModal from './CreateOrEditList'
import * as UserAddRemoveLists from './UserAddRemoveLists' import * as CropImageModal from './crop-image/CropImage.web'
import * as ListAddUserModal from './ListAddRemoveUsers'
import * as DeleteAccountModal from './DeleteAccount' import * as DeleteAccountModal from './DeleteAccount'
import * as EditImageModal from './EditImage'
import * as EditProfileModal from './EditProfile'
import * as InviteCodesModal from './InviteCodes'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as LinkWarningModal from './LinkWarning'
import * as ListAddUserModal from './ListAddRemoveUsers'
import * as RepostModal from './Repost' import * as RepostModal from './Repost'
import * as SelfLabelModal from './SelfLabel' import * as SelfLabelModal from './SelfLabel'
import * as ThreadgateModal from './Threadgate' import * as ThreadgateModal from './Threadgate'
import * as CropImageModal from './crop-image/CropImage.web' import * as UserAddRemoveLists from './UserAddRemoveLists'
import * as AltTextImageModal from './AltImage'
import * as EditImageModal from './EditImage'
import * as ChangeHandleModal from './ChangeHandle'
import * as InviteCodesModal from './InviteCodes'
import * as AddAppPassword from './AddAppPasswords'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
import * as VerifyEmailModal from './VerifyEmail' import * as VerifyEmailModal from './VerifyEmail'
import * as ChangeEmailModal from './ChangeEmail'
import * as ChangePasswordModal from './ChangePassword'
import * as LinkWarningModal from './LinkWarning'
import * as EmbedConsentModal from './EmbedConsent'
export function ModalsContainer() { export function ModalsContainer() {
const {isModalActive, activeModals} = useModals() const {isModalActive, activeModals} = useModals()
@@ -112,8 +111,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <ChangePasswordModal.Component /> element = <ChangePasswordModal.Component />
} else if (modal.name === 'link-warning') { } else if (modal.name === 'link-warning') {
element = <LinkWarningModal.Component {...modal} /> element = <LinkWarningModal.Component {...modal} />
} else if (modal.name === 'embed-consent') {
element = <EmbedConsentModal.Component {...modal} />
} else { } else {
return null return null
} }
+21 -7
View File
@@ -1,17 +1,22 @@
import React, {forwardRef} from 'react' import React, {forwardRef} from 'react'
import {Animated, View} from 'react-native' import {Animated, View} from 'react-native'
import PagerView, { import PagerView, {
PagerViewOnPageSelectedEvent,
PagerViewOnPageScrollEvent, PagerViewOnPageScrollEvent,
PagerViewOnPageSelectedEvent,
PageScrollStateChangedNativeEvent, PageScrollStateChangedNativeEvent,
} from 'react-native-pager-view' } from 'react-native-pager-view'
import {LogEvents} from '#/lib/statsig/events'
import {s} from 'lib/styles' import {s} from 'lib/styles'
export type PageSelectedEvent = PagerViewOnPageSelectedEvent export type PageSelectedEvent = PagerViewOnPageSelectedEvent
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
export interface PagerRef { export interface PagerRef {
setPage: (index: number) => void setPage: (
index: number,
reason: LogEvents['home:feedDisplayed']['reason'],
) => void
} }
export interface RenderTabBarFnProps { export interface RenderTabBarFnProps {
@@ -25,7 +30,10 @@ interface Props {
initialPage?: number initialPage?: number
renderTabBar: RenderTabBarFn renderTabBar: RenderTabBarFn
onPageSelected?: (index: number) => void onPageSelected?: (index: number) => void
onPageSelecting?: (index: number) => void onPageSelecting?: (
index: number,
reason: LogEvents['home:feedDisplayed']['reason'],
) => void
onPageScrollStateChanged?: ( onPageScrollStateChanged?: (
scrollState: 'idle' | 'dragging' | 'settling', scrollState: 'idle' | 'dragging' | 'settling',
) => void ) => void
@@ -51,7 +59,13 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
const pagerView = React.useRef<PagerView>(null) const pagerView = React.useRef<PagerView>(null)
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
setPage: (index: number) => pagerView.current?.setPage(index), setPage: (
index: number,
reason: LogEvents['home:feedDisplayed']['reason'],
) => {
pagerView.current?.setPage(index)
onPageSelecting?.(index, reason)
},
})) }))
const onPageSelectedInner = React.useCallback( const onPageSelectedInner = React.useCallback(
@@ -79,14 +93,14 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
// -prf // -prf
if (scrollState.current === 'settling') { if (scrollState.current === 'settling') {
if (lastDirection.current === -1 && offset < lastOffset.current) { if (lastDirection.current === -1 && offset < lastOffset.current) {
onPageSelecting?.(position) onPageSelecting?.(position, 'pager-swipe')
setSelectedPage(position) setSelectedPage(position)
lastDirection.current = 0 lastDirection.current = 0
} else if ( } else if (
lastDirection.current === 1 && lastDirection.current === 1 &&
offset > lastOffset.current offset > lastOffset.current
) { ) {
onPageSelecting?.(position + 1) onPageSelecting?.(position + 1, 'pager-swipe')
setSelectedPage(position + 1) setSelectedPage(position + 1)
lastDirection.current = 0 lastDirection.current = 0
} }
@@ -113,7 +127,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
const onTabBarSelect = React.useCallback( const onTabBarSelect = React.useCallback(
(index: number) => { (index: number) => {
pagerView.current?.setPage(index) pagerView.current?.setPage(index)
onPageSelecting?.(index) onPageSelecting?.(index, 'tabbar-click')
}, },
[pagerView, onPageSelecting], [pagerView, onPageSelecting],
) )
+16 -6
View File
@@ -1,6 +1,8 @@
import React from 'react' import React from 'react'
import {flushSync} from 'react-dom'
import {View} from 'react-native' import {View} from 'react-native'
import {flushSync} from 'react-dom'
import {LogEvents} from '#/lib/statsig/events'
import {s} from 'lib/styles' import {s} from 'lib/styles'
export interface RenderTabBarFnProps { export interface RenderTabBarFnProps {
@@ -14,7 +16,10 @@ interface Props {
initialPage?: number initialPage?: number
renderTabBar: RenderTabBarFn renderTabBar: RenderTabBarFn
onPageSelected?: (index: number) => void onPageSelected?: (index: number) => void
onPageSelecting?: (index: number) => void onPageSelecting?: (
index: number,
reason: LogEvents['home:feedDisplayed']['reason'],
) => void
} }
export const Pager = React.forwardRef(function PagerImpl( export const Pager = React.forwardRef(function PagerImpl(
{ {
@@ -31,11 +36,16 @@ export const Pager = React.forwardRef(function PagerImpl(
const anchorRef = React.useRef(null) const anchorRef = React.useRef(null)
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
setPage: (index: number) => onTabBarSelect(index), setPage: (
index: number,
reason: LogEvents['home:feedDisplayed']['reason'],
) => {
onTabBarSelect(index, reason)
},
})) }))
const onTabBarSelect = React.useCallback( const onTabBarSelect = React.useCallback(
(index: number) => { (index: number, reason: LogEvents['home:feedDisplayed']['reason']) => {
const scrollY = window.scrollY const scrollY = window.scrollY
// We want to determine if the tabbar is already "sticking" at the top (in which // We want to determine if the tabbar is already "sticking" at the top (in which
// case we should preserve and restore scroll), or if it is somewhere below in the // case we should preserve and restore scroll), or if it is somewhere below in the
@@ -54,7 +64,7 @@ export const Pager = React.forwardRef(function PagerImpl(
flushSync(() => { flushSync(() => {
setSelectedPage(index) setSelectedPage(index)
onPageSelected?.(index) onPageSelected?.(index)
onPageSelecting?.(index) onPageSelecting?.(index, reason)
}) })
if (isSticking) { if (isSticking) {
const restoredScrollY = scrollYs.current[index] const restoredScrollY = scrollYs.current[index]
@@ -73,7 +83,7 @@ export const Pager = React.forwardRef(function PagerImpl(
{renderTabBar({ {renderTabBar({
selectedPage, selectedPage,
tabBarAnchor: <View ref={anchorRef} />, tabBarAnchor: <View ref={anchorRef} />,
onSelect: onTabBarSelect, onSelect: e => onTabBarSelect(e, 'tabbar-click'),
})} })}
{React.Children.map(children, (child, i) => ( {React.Children.map(children, (child, i) => (
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}> <View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
@@ -1,6 +1,4 @@
import {EmbedPlayerParams, getGifDims} from 'lib/strings/embed-player'
import React from 'react' import React from 'react'
import {Image, ImageLoadEventData} from 'expo-image'
import { import {
ActivityIndicator, ActivityIndicator,
GestureResponderEvent, GestureResponderEvent,
@@ -9,13 +7,17 @@ import {
StyleSheet, StyleSheet,
View, View,
} from 'react-native' } from 'react-native'
import {isIOS, isNative, isWeb} from '#/platform/detection' import {Image, ImageLoadEventData} from 'expo-image'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useExternalEmbedsPrefs} from 'state/preferences'
import {useModalControls} from 'state/modals'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {AppBskyEmbedExternal} from '@atproto/api' import {AppBskyEmbedExternal} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {EmbedPlayerParams, getGifDims} from '#/lib/strings/embed-player'
import {isIOS, isNative, isWeb} from '#/platform/detection'
import {useExternalEmbedsPrefs} from '#/state/preferences'
import {useDialogControl} from '#/components/Dialog'
import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
export function ExternalGifEmbed({ export function ExternalGifEmbed({
link, link,
@@ -25,8 +27,9 @@ export function ExternalGifEmbed({
params: EmbedPlayerParams params: EmbedPlayerParams
}) { }) {
const externalEmbedsPrefs = useExternalEmbedsPrefs() const externalEmbedsPrefs = useExternalEmbedsPrefs()
const {openModal} = useModalControls()
const {_} = useLingui() const {_} = useLingui()
const consentDialogControl = useDialogControl()
const thumbHasLoaded = React.useRef(false) const thumbHasLoaded = React.useRef(false)
const viewWidth = React.useRef(0) const viewWidth = React.useRef(0)
@@ -57,11 +60,7 @@ export function ExternalGifEmbed({
// Show consent if this is the first load // Show consent if this is the first load
if (externalEmbedsPrefs?.[params.source] === undefined) { if (externalEmbedsPrefs?.[params.source] === undefined) {
openModal({ consentDialogControl.open()
name: 'embed-consent',
source: params.source,
onAccept: load,
})
return return
} }
// If the player isn't active, we want to activate it and prefetch the gif // If the player isn't active, we want to activate it and prefetch the gif
@@ -84,7 +83,13 @@ export function ExternalGifEmbed({
} }
}) })
}, },
[externalEmbedsPrefs, isPlayerActive, load, openModal, params.source], [
consentDialogControl,
externalEmbedsPrefs,
isPlayerActive,
load,
params.source,
],
) )
const onLoad = React.useCallback((e: ImageLoadEventData) => { const onLoad = React.useCallback((e: ImageLoadEventData) => {
@@ -98,47 +103,55 @@ export function ExternalGifEmbed({
}, []) }, [])
return ( return (
<Pressable <>
style={[ <EmbedConsentDialog
{height: imageDims.height}, control={consentDialogControl}
styles.topRadius, source={params.source}
styles.gifContainer, onAccept={load}
]}
onPress={onPlayPress}
onLayout={onLayout}
accessibilityRole="button"
accessibilityHint={_(msg`Plays the GIF`)}
accessibilityLabel={_(msg`Play ${link.title}`)}>
{(!isPrefetched || !isAnimating) && ( // If we have not loaded or are not animating, show the overlay
<View style={[styles.layer, styles.overlayLayer]}>
<View style={[styles.overlayContainer, styles.topRadius]}>
{!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active
<FontAwesomeIcon icon="play" size={42} color="white" />
) : (
// Activity indicator while gif loads
<ActivityIndicator size="large" color="white" />
)}
</View>
</View>
)}
<Image
source={{
uri:
!isPrefetched || (isWeb && !isAnimating)
? link.thumb
: params.playerUri,
}} // Web uses the thumb to control playback
style={{flex: 1}}
ref={imageRef}
onLoad={onLoad}
autoplay={isAnimating}
contentFit="contain"
accessibilityIgnoresInvertColors
accessibilityLabel={link.title}
accessibilityHint={link.title}
cachePolicy={isIOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios
/> />
</Pressable>
<Pressable
style={[
{height: imageDims.height},
styles.topRadius,
styles.gifContainer,
]}
onPress={onPlayPress}
onLayout={onLayout}
accessibilityRole="button"
accessibilityHint={_(msg`Plays the GIF`)}
accessibilityLabel={_(msg`Play ${link.title}`)}>
{(!isPrefetched || !isAnimating) && ( // If we have not loaded or are not animating, show the overlay
<View style={[styles.layer, styles.overlayLayer]}>
<View style={[styles.overlayContainer, styles.topRadius]}>
{!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active
<FontAwesomeIcon icon="play" size={42} color="white" />
) : (
// Activity indicator while gif loads
<ActivityIndicator size="large" color="white" />
)}
</View>
</View>
)}
<Image
source={{
uri:
!isPrefetched || (isWeb && !isAnimating)
? link.thumb
: params.playerUri,
}} // Web uses the thumb to control playback
style={{flex: 1}}
ref={imageRef}
onLoad={onLoad}
autoplay={isAnimating}
contentFit="contain"
accessibilityIgnoresInvertColors
accessibilityLabel={link.title}
accessibilityHint={link.title}
cachePolicy={isIOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios
/>
</Pressable>
</>
) )
} }
@@ -13,20 +13,23 @@ import Animated, {
useAnimatedRef, useAnimatedRef,
useFrameCallback, useFrameCallback,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {Image} from 'expo-image'
import {WebView} from 'react-native-webview'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {WebView} from 'react-native-webview'
import {Image} from 'expo-image'
import {AppBskyEmbedExternal} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {AppBskyEmbedExternal} from '@atproto/api'
import {EmbedPlayerParams, getPlayerAspect} from 'lib/strings/embed-player' import {NavigationProp} from '#/lib/routes/types'
import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player'
import {isNative} from '#/platform/detection'
import {useExternalEmbedsPrefs} from '#/state/preferences'
import {atoms as a} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
import {EventStopper} from '../EventStopper' import {EventStopper} from '../EventStopper'
import {isNative} from 'platform/detection'
import {NavigationProp} from 'lib/routes/types'
import {useExternalEmbedsPrefs} from 'state/preferences'
import {useModalControls} from 'state/modals'
interface ShouldStartLoadRequest { interface ShouldStartLoadRequest {
url: string url: string
@@ -48,7 +51,7 @@ function PlaceholderOverlay({
if (isPlayerActive && !isLoading) return null if (isPlayerActive && !isLoading) return null
return ( return (
<View style={[styles.layer, styles.overlayLayer]}> <View style={[a.absolute, a.inset_0, styles.overlayLayer]}>
<Pressable <Pressable
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Play Video`)} accessibilityLabel={_(msg`Play Video`)}
@@ -89,7 +92,7 @@ function Player({
if (!isPlayerActive) return null if (!isPlayerActive) return null
return ( return (
<EventStopper style={[styles.layer, styles.playerLayer]}> <EventStopper style={[a.absolute, a.inset_0, styles.playerLayer]}>
<WebView <WebView
javaScriptEnabled={true} javaScriptEnabled={true}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
@@ -119,7 +122,7 @@ export function ExternalPlayer({
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const windowDims = useWindowDimensions() const windowDims = useWindowDimensions()
const externalEmbedsPrefs = useExternalEmbedsPrefs() const externalEmbedsPrefs = useExternalEmbedsPrefs()
const {openModal} = useModalControls() const consentDialogControl = useDialogControl()
const [isPlayerActive, setPlayerActive] = React.useState(false) const [isPlayerActive, setPlayerActive] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(true) const [isLoading, setIsLoading] = React.useState(true)
@@ -187,37 +190,47 @@ export function ExternalPlayer({
event.preventDefault() event.preventDefault()
if (externalEmbedsPrefs?.[params.source] === undefined) { if (externalEmbedsPrefs?.[params.source] === undefined) {
openModal({ consentDialogControl.open()
name: 'embed-consent',
source: params.source,
onAccept: () => {
setPlayerActive(true)
},
})
return return
} }
setPlayerActive(true) setPlayerActive(true)
}, },
[externalEmbedsPrefs, openModal, params.source], [externalEmbedsPrefs, consentDialogControl, params.source],
) )
const onAcceptConsent = React.useCallback(() => {
setPlayerActive(true)
}, [])
return ( return (
<Animated.View ref={viewRef} collapsable={false} style={[aspect]}> <>
{link.thumb && (!isPlayerActive || isLoading) && ( <EmbedConsentDialog
<Image control={consentDialogControl}
style={[{flex: 1}, styles.topRadius]} source={params.source}
source={{uri: link.thumb}} onAccept={onAcceptConsent}
accessibilityIgnoresInvertColors
/>
)}
<PlaceholderOverlay
isLoading={isLoading}
isPlayerActive={isPlayerActive}
onPress={onPlayPress}
/> />
<Player isPlayerActive={isPlayerActive} params={params} onLoad={onLoad} />
</Animated.View> <Animated.View ref={viewRef} collapsable={false} style={[aspect]}>
{link.thumb && (!isPlayerActive || isLoading) && (
<Image
style={[a.flex_1, styles.topRadius]}
source={{uri: link.thumb}}
accessibilityIgnoresInvertColors
/>
)}
<PlaceholderOverlay
isLoading={isLoading}
isPlayerActive={isPlayerActive}
onPress={onPlayPress}
/>
<Player
isPlayerActive={isPlayerActive}
params={params}
onLoad={onLoad}
/>
</Animated.View>
</>
) )
} }
@@ -226,13 +239,6 @@ const styles = StyleSheet.create({
borderTopLeftRadius: 6, borderTopLeftRadius: 6,
borderTopRightRadius: 6, borderTopRightRadius: 6,
}, },
layer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
overlayContainer: { overlayContainer: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
+28 -2
View File
@@ -2,8 +2,9 @@ import React from 'react'
import {ActivityIndicator, AppState, StyleSheet, View} from 'react-native' import {ActivityIndicator, AppState, StyleSheet, View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {useGate} from '#/lib/statsig/statsig' import {logEvent, LogEvents, useGate} from '#/lib/statsig/statsig'
import {emitSoftReset} from '#/state/events' import {emitSoftReset} from '#/state/events'
import {FeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed' import {FeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
@@ -79,7 +80,7 @@ function HomeScreenReady({
// This is supposed to only happen on the web when you use the right nav. // This is supposed to only happen on the web when you use the right nav.
if (selectedIndex !== lastPagerReportedIndexRef.current) { if (selectedIndex !== lastPagerReportedIndexRef.current) {
lastPagerReportedIndexRef.current = selectedIndex lastPagerReportedIndexRef.current = selectedIndex
pagerRef.current?.setPage(selectedIndex) pagerRef.current?.setPage(selectedIndex, 'desktop-sidebar-click')
} }
}, [selectedIndex]) }, [selectedIndex])
@@ -96,6 +97,17 @@ function HomeScreenReady({
}, [setDrawerSwipeDisabled, selectedIndex, setMinimalShellMode]), }, [setDrawerSwipeDisabled, selectedIndex, setMinimalShellMode]),
) )
useFocusEffect(
useNonReactiveCallback(() => {
logEvent('home:feedDisplayed', {
index: selectedIndex,
feedType: selectedFeed.split('|')[0],
feedUrl: selectedFeed,
reason: 'focus',
})
}),
)
const disableMinShellOnForegrounding = useGate( const disableMinShellOnForegrounding = useGate(
'disable_min_shell_on_foregrounding', 'disable_min_shell_on_foregrounding',
) )
@@ -123,6 +135,19 @@ function HomeScreenReady({
[setDrawerSwipeDisabled, setSelectedFeed, setMinimalShellMode, allFeeds], [setDrawerSwipeDisabled, setSelectedFeed, setMinimalShellMode, allFeeds],
) )
const onPageSelecting = React.useCallback(
(index: number, reason: LogEvents['home:feedDisplayed']['reason']) => {
const feed = allFeeds[index]
logEvent('home:feedDisplayed', {
index,
feedType: feed.split('|')[0],
feedUrl: feed,
reason,
})
},
[allFeeds],
)
const onPressSelected = React.useCallback(() => { const onPressSelected = React.useCallback(() => {
emitSoftReset() emitSoftReset()
}, []) }, [])
@@ -175,6 +200,7 @@ function HomeScreenReady({
ref={pagerRef} ref={pagerRef}
testID="homeScreen" testID="homeScreen"
initialPage={selectedIndex} initialPage={selectedIndex}
onPageSelecting={onPageSelecting}
onPageSelected={onPageSelected} onPageSelected={onPageSelected}
onPageScrollStateChanged={onPageScrollStateChanged} onPageScrollStateChanged={onPageScrollStateChanged}
renderTabBar={renderTabBar}> renderTabBar={renderTabBar}>
+136 -1
View File
@@ -6,12 +6,13 @@ import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {H3, P} from '#/components/Typography' import {H3, P, Text} from '#/components/Typography'
export function Dialogs() { export function Dialogs() {
const scrollable = Dialog.useDialogControl() const scrollable = Dialog.useDialogControl()
const basic = Dialog.useDialogControl() const basic = Dialog.useDialogControl()
const prompt = Prompt.usePromptControl() const prompt = Prompt.usePromptControl()
const testDialog = Dialog.useDialogControl()
const {closeAllDialogs} = useDialogStateControlContext() const {closeAllDialogs} = useDialogStateControlContext()
return ( return (
@@ -60,6 +61,15 @@ export function Dialogs() {
<ButtonText>Open prompt</ButtonText> <ButtonText>Open prompt</ButtonText>
</Button> </Button>
<Button
variant="solid"
color="primary"
size="small"
onPress={testDialog.open}
label="one">
<ButtonText>Open Tester</ButtonText>
</Button>
<Prompt.Outer control={prompt}> <Prompt.Outer control={prompt}>
<Prompt.TitleText>This is a prompt</Prompt.TitleText> <Prompt.TitleText>This is a prompt</Prompt.TitleText>
<Prompt.DescriptionText> <Prompt.DescriptionText>
@@ -122,6 +132,131 @@ export function Dialogs() {
</View> </View>
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
</Dialog.Outer> </Dialog.Outer>
<Dialog.Outer control={testDialog}>
<Dialog.Handle />
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<View style={[a.relative, a.gap_md, a.w_full]}>
<Text>
Watch the console logs to test each of these dialog edge cases.
Functionality should be consistent across both native and web. If
not then *sad face* something is wrong.
</Text>
<Button
variant="outline"
color="primary"
size="small"
onPress={() => {
testDialog.close(() => {
console.log('close callback')
})
}}
label="Close It">
<ButtonText>Normal Use (Should just log)</ButtonText>
</Button>
<Button
variant="outline"
color="primary"
size="small"
onPress={() => {
testDialog.close(() => {
console.log('close callback')
})
setTimeout(() => {
testDialog.open()
}, 100)
}}
label="Close It">
<ButtonText>
Calls `.open()` in 100ms (Should log when the animation switches
to open)
</ButtonText>
</Button>
<Button
variant="outline"
color="primary"
size="small"
onPress={() => {
setTimeout(() => {
testDialog.open()
}, 2e3)
testDialog.close(() => {
console.log('close callback')
})
}}
label="Close It">
<ButtonText>
Calls `.open()` in 2000ms (Should log after close animation and
not log on open)
</ButtonText>
</Button>
<Button
variant="outline"
color="primary"
size="small"
onPress={() => {
testDialog.close(() => {
console.log('close callback')
})
setTimeout(() => {
testDialog.close(() => {
console.log('close callback after 100ms')
})
}, 100)
}}
label="Close It">
<ButtonText>
Calls `.close()` then again in 100ms (should log twice)
</ButtonText>
</Button>
<Button
variant="outline"
color="primary"
size="small"
onPress={() => {
testDialog.close(() => {
console.log('close callback')
})
testDialog.close(() => {
console.log('close callback 2')
})
}}
label="Close It">
<ButtonText>
Call `close()` twice immediately (should just log twice)
</ButtonText>
</Button>
<Button
variant="outline"
color="primary"
size="small"
onPress={() => {
console.log('Step 1')
testDialog.close(() => {
console.log('Step 3')
})
console.log('Step 2')
}}
label="Close It">
<ButtonText>
Log before `close()`, after `close()` and in the `close()`
callback. Should be an order of 1 2 3
</ButtonText>
</Button>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
</View> </View>
) )
} }
+225 -197
View File
@@ -46,28 +46,14 @@
multiformats "^9.9.0" multiformats "^9.9.0"
tlds "^1.234.0" tlds "^1.234.0"
"@atproto/api@^0.9.5": "@atproto/aws@^0.2.0":
version "0.9.5" version "0.2.0"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.9.5.tgz#630e5d9520bba38d0cd348c8028ddbb73bd074f8" resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.0.tgz#17f3faf744824457cabd62f87be8bf08cacf8029"
integrity sha512-4vlwTbiWSkCV0DkfNMawiH+26Fv7txPr4x0vwq6KPIBz28UHPK9UyPseLKxi6/Aok74aPr8ySJ4+nfcmwcp08Q== integrity sha512-F09SHiC9CX3ydfrvYZbkpfES48UGCQNnznNVgJ3QyKSN8ON+BoWmGCpAFtn3AWeEoU0w9h0hypNvUm5nORv+5g==
dependencies: dependencies:
"@atproto/common-web" "^0.2.3" "@atproto/common" "^0.4.0"
"@atproto/lexicon" "^0.3.1" "@atproto/crypto" "^0.4.0"
"@atproto/syntax" "^0.1.5" "@atproto/repo" "^0.4.0"
"@atproto/xrpc" "^0.4.1"
multiformats "^9.9.0"
tlds "^1.234.0"
typed-emitter "^2.1.0"
zod "^3.21.4"
"@atproto/aws@^0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.1.6.tgz#c6ecbfd92b325f3c5433688534d47f43358b415b"
integrity sha512-ZWfETjv9ku/5gxs3SVVW1sbGsQMmdnwmjp3RCxXFhMgNkbNL2l7xb17nRQ1VibsFLBWpp0+MeFTZYlSEzAYfCw==
dependencies:
"@atproto/common" "^0.3.3"
"@atproto/crypto" "^0.3.0"
"@atproto/repo" "^0.3.6"
"@aws-sdk/client-cloudfront" "^3.261.0" "@aws-sdk/client-cloudfront" "^3.261.0"
"@aws-sdk/client-kms" "^3.196.0" "@aws-sdk/client-kms" "^3.196.0"
"@aws-sdk/client-s3" "^3.224.0" "@aws-sdk/client-s3" "^3.224.0"
@@ -77,71 +63,58 @@
multiformats "^9.9.0" multiformats "^9.9.0"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/bsky@^0.0.28": "@atproto/bsky@^0.0.44":
version "0.0.28" version "0.0.44"
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.28.tgz#d9516f682883ceba60f52e3944d93dbd81375a7e" resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.44.tgz#990d6061d557cdf891d43656543ebb611f57bd82"
integrity sha512-Sq5/UWyxtIf/7UCPIHTcvE1ZUgWm+kYMkQVYRkMnNK5nN1G3/nY8Sm21qcVO/jxItOyua64XFoRss1zJf8G+Bw== integrity sha512-SVOnvdUlDf9sKI1Tto+IY1tVS4/9VRoTTiI08ezvK9sew9sQVUVurwYI5E3EtAbEi3ukBPZ9+Cuoh3Me65iyjQ==
dependencies: dependencies:
"@atproto/api" "^0.9.5" "@atproto/api" "^0.12.2"
"@atproto/common" "^0.3.3" "@atproto/common" "^0.4.0"
"@atproto/crypto" "^0.3.0" "@atproto/crypto" "^0.4.0"
"@atproto/identity" "^0.3.2" "@atproto/identity" "^0.4.0"
"@atproto/lexicon" "^0.3.1" "@atproto/lexicon" "^0.4.0"
"@atproto/repo" "^0.3.6" "@atproto/repo" "^0.4.0"
"@atproto/syntax" "^0.1.5" "@atproto/syntax" "^0.3.0"
"@atproto/xrpc-server" "^0.4.2" "@atproto/xrpc-server" "^0.5.1"
"@bufbuild/protobuf" "^1.5.0" "@bufbuild/protobuf" "^1.5.0"
"@connectrpc/connect" "^1.1.4" "@connectrpc/connect" "^1.1.4"
"@connectrpc/connect-express" "^1.1.4"
"@connectrpc/connect-node" "^1.1.4" "@connectrpc/connect-node" "^1.1.4"
"@did-plc/lib" "^0.0.1" "@did-plc/lib" "^0.0.1"
"@isaacs/ttlcache" "^1.4.1"
compression "^1.7.4" compression "^1.7.4"
cors "^2.8.5" cors "^2.8.5"
express "^4.17.2" express "^4.17.2"
express-async-errors "^3.1.1"
form-data "^4.0.0"
http-errors "^2.0.0" http-errors "^2.0.0"
http-terminator "^3.2.0" http-terminator "^3.2.0"
ioredis "^5.3.2" ioredis "^5.3.2"
jose "^5.0.1"
kysely "^0.22.0" kysely "^0.22.0"
multiformats "^9.9.0" multiformats "^9.9.0"
murmurhash "^2.0.1"
p-queue "^6.6.2" p-queue "^6.6.2"
pg "^8.10.0" pg "^8.10.0"
pino "^8.15.0" pino "^8.15.0"
pino-http "^8.2.1" pino-http "^8.2.1"
sharp "^0.32.6" sharp "^0.32.6"
structured-headers "^1.0.1"
typed-emitter "^2.1.0" typed-emitter "^2.1.0"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/bsync@^0.0.0": "@atproto/bsync@^0.0.3":
version "0.0.0" version "0.0.3"
resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.0.tgz#b08160ee8aca7d9fd9d8dc34a4719227b518df9d" resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.3.tgz#2b0b8ef3686cf177846a80088317f2e89d1bf88f"
integrity sha512-gv0dOnKGPhB0xyqLJhu3U3osZAPXLnaZQTRzwZlC5tm/Yc+c8myv2E3nIF+3Ojekh/cbg9SC8qRae4pEL0WHYg== integrity sha512-tJRwNgXzfNV57lzgWPvjtb1OMlMJH9SpsMeYhIii16zcaFUWwsb474BicKpkGRT+iCvtYzBT6gWlZE2Ijnhf7w==
dependencies: dependencies:
"@atproto/common" "^0.3.3" "@atproto/common" "^0.4.0"
"@atproto/syntax" "^0.1.5" "@atproto/syntax" "^0.3.0"
"@bufbuild/protobuf" "^1.5.0" "@bufbuild/protobuf" "^1.5.0"
"@connectrpc/connect" "^1.1.4" "@connectrpc/connect" "^1.1.4"
"@connectrpc/connect-express" "^1.1.4"
"@connectrpc/connect-node" "^1.1.4" "@connectrpc/connect-node" "^1.1.4"
http-terminator "^3.2.0" http-terminator "^3.2.0"
kysely "^0.22.0" kysely "^0.22.0"
pg "^8.10.0" pg "^8.10.0"
pino "^8.15.0"
pino-http "^8.2.1" pino-http "^8.2.1"
typed-emitter "^2.1.0" typed-emitter "^2.1.0"
"@atproto/common-web@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.2.3.tgz#c44c1e177ae8309d5116347d49850209e8e478cc"
integrity sha512-k9VKGYUqjsRlI3wS31XyCbeb2U7ddS4X/eFgzos2CE5rIbk/uQGyKH+0Jcn1JIwRkvI1BemyNuUVrS8Ok3wiuw==
dependencies:
graphemer "^1.4.0"
multiformats "^9.9.0"
uint8arrays "3.0.0"
zod "^3.21.4"
"@atproto/common-web@^0.3.0": "@atproto/common-web@^0.3.0":
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.0.tgz#36da8c2c31d8cf8a140c3c8f03223319bf4430bb" resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.0.tgz#36da8c2c31d8cf8a140c3c8f03223319bf4430bb"
@@ -172,18 +145,17 @@
pino "^8.6.1" pino "^8.6.1"
zod "^3.14.2" zod "^3.14.2"
"@atproto/common@^0.3.3": "@atproto/common@^0.4.0":
version "0.3.3" version "0.4.0"
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.3.3.tgz#bc0059929e528032a55aa32fd180c6e992959dd6" resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.0.tgz#d77696c7eb545426df727837d9ee333b429fe7ef"
integrity sha512-ETYsHpQoytW3yJ1BoMDCZh3tdokV3HbZ2ThXq+EWbMxbGNsRDREgJK3JXJMHapf8PrnZZpE2VdWM9NHvlcmnQg== integrity sha512-yOXuPlCjT/OK9j+neIGYn9wkxx/AlxQSucysAF0xgwu0Ji8jAtKBf9Jv6R5ObYAjAD/kVUvEYumle+Yq/R9/7g==
dependencies: dependencies:
"@atproto/common-web" "^0.2.3" "@atproto/common-web" "^0.3.0"
"@ipld/dag-cbor" "^7.0.3" "@ipld/dag-cbor" "^7.0.3"
cbor-x "^1.5.1" cbor-x "^1.5.1"
iso-datestring-validator "^2.2.2" iso-datestring-validator "^2.2.2"
multiformats "^9.9.0" multiformats "^9.9.0"
pino "^8.15.0" pino "^8.15.0"
zod "3.21.4"
"@atproto/crypto@0.1.0": "@atproto/crypto@0.1.0":
version "0.1.0" version "0.1.0"
@@ -196,63 +168,49 @@
one-webcrypto "^1.0.3" one-webcrypto "^1.0.3"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/crypto@^0.3.0": "@atproto/crypto@^0.4.0":
version "0.3.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.3.0.tgz#a79e05a85129810755f3456e9d419b49824407d7" resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.4.0.tgz#dcdd6bf5ba98261ae0ff3b96d7b8695c1ef788e6"
integrity sha512-bhcxRTL4fgRY2YX/St0x4o0oDUp18QIPD7ek+7v8UKA0HpsCGQYbo8w9d9hUvwwty5X5p00cYF2tbggUWaPy7A== integrity sha512-Kj/4VgJ7hzzXvE42L0rjzP6lM0tai+OfPnP1rxJ+UZg/YUDtuewL4uapnVoWXvlNceKgaLZH98g5n9gXBVTe5Q==
dependencies: dependencies:
"@noble/curves" "^1.1.0" "@noble/curves" "^1.1.0"
"@noble/hashes" "^1.3.1" "@noble/hashes" "^1.3.1"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/dev-env@^0.2.28": "@atproto/dev-env@^0.3.4":
version "0.2.28" version "0.3.4"
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.2.28.tgz#c7ed2f34af28fab7c02da85155f0e2dcd8ce447a" resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.4.tgz#153b7be8268b2dcfc8d0ba4abc5fd60ad7a6e241"
integrity sha512-RD6USl0m7usHl1MyuCf3dZPuaJFnlEfo3Eni5mRZQNTPgS1ItZcrjEYsd8djmA2RmndCOC2FMBNhemALN6uRMw== integrity sha512-ix33GBQ1hjesoieTQKx38VGxZWNKeXCnaMdalr0/SAFwaDPCqMOrvUTPCx8VWClgAd0qYMcBM98+0lBTohW1qQ==
dependencies: dependencies:
"@atproto/api" "^0.9.5" "@atproto/api" "^0.12.2"
"@atproto/bsky" "^0.0.28" "@atproto/bsky" "^0.0.44"
"@atproto/bsync" "^0.0.0" "@atproto/bsync" "^0.0.3"
"@atproto/common-web" "^0.2.3" "@atproto/common-web" "^0.3.0"
"@atproto/crypto" "^0.3.0" "@atproto/crypto" "^0.4.0"
"@atproto/identity" "^0.3.2" "@atproto/identity" "^0.4.0"
"@atproto/lexicon" "^0.3.1" "@atproto/lexicon" "^0.4.0"
"@atproto/ozone" "^0.0.7" "@atproto/ozone" "^0.1.6"
"@atproto/pds" "^0.3.16" "@atproto/pds" "^0.4.13"
"@atproto/syntax" "^0.1.5" "@atproto/syntax" "^0.3.0"
"@atproto/xrpc-server" "^0.4.2" "@atproto/xrpc-server" "^0.5.1"
"@did-plc/lib" "^0.0.1" "@did-plc/lib" "^0.0.1"
"@did-plc/server" "^0.0.1" "@did-plc/server" "^0.0.1"
axios "^0.27.2" axios "^0.27.2"
better-sqlite3 "^7.6.2"
chalk "^5.0.1"
dotenv "^16.0.3" dotenv "^16.0.3"
express "^4.18.2" express "^4.18.2"
get-port "^6.1.2" get-port "^5.1.1"
multiformats "^9.9.0" multiformats "^9.9.0"
sharp "^0.32.6"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/identity@^0.3.2": "@atproto/identity@^0.4.0":
version "0.3.2" version "0.4.0"
resolved "https://registry.yarnpkg.com/@atproto/identity/-/identity-0.3.2.tgz#8a0536bc19ccbc45a04df84c3f30d86f58f964ee" resolved "https://registry.yarnpkg.com/@atproto/identity/-/identity-0.4.0.tgz#f8a4d450a20606d221c4ec05b856c0ce55f0a3a7"
integrity sha512-xZSyB3gHn/avwdAIV+mECvjjvMYXxPSvSgVBUsETMvMY72H9d84utDD58y5aAU/9mL+founaNZmniKDaR633CQ== integrity sha512-KKdVlqBgkFuTUx3KFiiQe0LuK9kopej1bhKm6SHRPEYbSEPFmRZQMY9TAjWJQrvQt8DpQzz6kVGjASFEjd3teQ==
dependencies: dependencies:
"@atproto/common-web" "^0.2.3" "@atproto/common-web" "^0.3.0"
"@atproto/crypto" "^0.3.0" "@atproto/crypto" "^0.4.0"
axios "^0.27.2" axios "^0.27.2"
"@atproto/lexicon@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.3.1.tgz#5d7275d041883a1c930404e3274a6fe7affc151f"
integrity sha512-yLy6GUNP4pn0mGUIyUHvN0UeBza0S03AgjTXVR6KliC4ut2+7SjNMe4cI4G1M8/bJMaccC6ooQSm2kvwiOdr3A==
dependencies:
"@atproto/common-web" "^0.2.3"
"@atproto/syntax" "^0.1.5"
iso-datestring-validator "^2.2.2"
multiformats "^9.9.0"
zod "^3.21.4"
"@atproto/lexicon@^0.4.0": "@atproto/lexicon@^0.4.0":
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.0.tgz#63e8829945d80c25524882caa8ed27b1151cc576" resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.0.tgz#63e8829945d80c25524882caa8ed27b1151cc576"
@@ -264,50 +222,50 @@
multiformats "^9.9.0" multiformats "^9.9.0"
zod "^3.21.4" zod "^3.21.4"
"@atproto/ozone@^0.0.7": "@atproto/ozone@^0.1.6":
version "0.0.7" version "0.1.6"
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.0.7.tgz#bfad82bc1d0900e79401a82f13581f707415505a" resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.6.tgz#b54c68360af19bfe6914d74b58759df0729461de"
integrity sha512-XffjEBoNV0uXimtrnGdn3PTy0BAMGLrIExa8XuIDH5ZKOUmYlyepWA0VG0IhNIWWXOSdDltw0mFi9D5ViXsBow== integrity sha512-uAXhXdO75vU/VVGGrsifZfaq6h7cMbEdS3bH8GCJfgwtxOlCU0elV2YM88GHBfVGJ0ghYKNki+Dhvpe8i+Fe1Q==
dependencies: dependencies:
"@atproto/api" "^0.9.5" "@atproto/api" "^0.12.2"
"@atproto/common" "^0.3.3" "@atproto/common" "^0.4.0"
"@atproto/crypto" "^0.3.0" "@atproto/crypto" "^0.4.0"
"@atproto/identity" "^0.3.2" "@atproto/identity" "^0.4.0"
"@atproto/lexicon" "^0.3.1" "@atproto/lexicon" "^0.4.0"
"@atproto/syntax" "^0.1.5" "@atproto/syntax" "^0.3.0"
"@atproto/xrpc-server" "^0.4.2" "@atproto/xrpc" "^0.5.0"
"@atproto/xrpc-server" "^0.5.1"
"@did-plc/lib" "^0.0.1" "@did-plc/lib" "^0.0.1"
axios "^1.6.7"
compression "^1.7.4" compression "^1.7.4"
cors "^2.8.5" cors "^2.8.5"
express "^4.17.2" express "^4.17.2"
express-async-errors "^3.1.1"
http-terminator "^3.2.0" http-terminator "^3.2.0"
kysely "^0.22.0" kysely "^0.22.0"
multiformats "^9.9.0" multiformats "^9.9.0"
p-queue "^6.6.2" p-queue "^6.6.2"
pg "^8.10.0" pg "^8.10.0"
pino "^8.15.0"
pino-http "^8.2.1" pino-http "^8.2.1"
typed-emitter "^2.1.0" typed-emitter "^2.1.0"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/pds@^0.3.16": "@atproto/pds@^0.4.13":
version "0.3.16" version "0.4.13"
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.3.16.tgz#5eb740934c1dc4cafeb6c57c2b857777c7cdfc0d" resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.13.tgz#9235d2c748d142a06d78da143ff1ad7e150b2d97"
integrity sha512-+DwRYn3FBiCd/Nu/F3+onoFdtL67zYcVDYIl1Aq6jOXNzMyzQW+Z4Y33OsKz5SMHJMuri4wef2iy63nCdSGxtw== integrity sha512-86fmaSFBP1HML0U85bsYkd06oO6XFFA/+VpRMeABy7cUShvvlkVq8anxp301Qaf89t+AM/tvjICqQ2syW8bgfA==
dependencies: dependencies:
"@atproto/api" "^0.9.5" "@atproto/api" "^0.12.2"
"@atproto/aws" "^0.1.6" "@atproto/aws" "^0.2.0"
"@atproto/common" "^0.3.3" "@atproto/common" "^0.4.0"
"@atproto/crypto" "^0.3.0" "@atproto/crypto" "^0.4.0"
"@atproto/identity" "^0.3.2" "@atproto/identity" "^0.4.0"
"@atproto/lexicon" "^0.3.1" "@atproto/lexicon" "^0.4.0"
"@atproto/repo" "^0.3.6" "@atproto/repo" "^0.4.0"
"@atproto/syntax" "^0.1.5" "@atproto/syntax" "^0.3.0"
"@atproto/xrpc" "^0.4.1" "@atproto/xrpc" "^0.5.0"
"@atproto/xrpc-server" "^0.4.2" "@atproto/xrpc-server" "^0.5.1"
"@did-plc/lib" "^0.0.4" "@did-plc/lib" "^0.0.4"
better-sqlite3 "^7.6.2" better-sqlite3 "^9.4.0"
bytes "^3.1.2" bytes "^3.1.2"
compression "^1.7.4" compression "^1.7.4"
cors "^2.8.5" cors "^2.8.5"
@@ -315,9 +273,8 @@
express "^4.17.2" express "^4.17.2"
express-async-errors "^3.1.1" express-async-errors "^3.1.1"
file-type "^16.5.4" file-type "^16.5.4"
form-data "^4.0.0" glob "^10.3.10"
handlebars "^4.7.7" handlebars "^4.7.7"
http-errors "^2.0.0"
http-terminator "^3.2.0" http-terminator "^3.2.0"
ioredis "^5.3.2" ioredis "^5.3.2"
jose "^5.0.1" jose "^5.0.1"
@@ -327,7 +284,6 @@
nodemailer "^6.8.0" nodemailer "^6.8.0"
nodemailer-html-to-text "^3.2.0" nodemailer-html-to-text "^3.2.0"
p-queue "^6.6.2" p-queue "^6.6.2"
pg "^8.10.0"
pino "^8.15.0" pino "^8.15.0"
pino-http "^8.2.1" pino-http "^8.2.1"
sharp "^0.32.6" sharp "^0.32.6"
@@ -335,43 +291,34 @@
uint8arrays "3.0.0" uint8arrays "3.0.0"
zod "^3.21.4" zod "^3.21.4"
"@atproto/repo@^0.3.6": "@atproto/repo@^0.4.0":
version "0.3.6" version "0.4.0"
resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.3.6.tgz#1732a5fdc71899be819b32b9c63c03815af4a6ce" resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.4.0.tgz#e5d3195a8e4233c9bf060737b18ddee905af2d9a"
integrity sha512-coCYHl/0V3ucyJ2Rgx/l0MK37ui3doph7AsDn8UFxrDeZrecOgkKu66/Zo3PtdmaAQfUPDWdx4cifKeX7IRD5Q== integrity sha512-LB0DF/D8r8hB+qiGB0sWZuq7TSJYbWel+t572aCrLeCOmbRgnLkGPLUTOOUvLFYv8xz1BPZTbI8hy/vcUV79VA==
dependencies: dependencies:
"@atproto/common" "^0.3.3" "@atproto/common" "^0.4.0"
"@atproto/common-web" "^0.2.3" "@atproto/common-web" "^0.3.0"
"@atproto/crypto" "^0.3.0" "@atproto/crypto" "^0.4.0"
"@atproto/identity" "^0.3.2" "@atproto/lexicon" "^0.4.0"
"@atproto/lexicon" "^0.3.1"
"@atproto/syntax" "^0.1.5"
"@ipld/car" "^3.2.3" "@ipld/car" "^3.2.3"
"@ipld/dag-cbor" "^7.0.0" "@ipld/dag-cbor" "^7.0.0"
multiformats "^9.9.0" multiformats "^9.9.0"
uint8arrays "3.0.0" uint8arrays "3.0.0"
zod "^3.21.4" zod "^3.21.4"
"@atproto/syntax@^0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.1.5.tgz#85b6488a33da3b864e8ac22a61b5586b271206ee"
integrity sha512-pbY5lOnThoAbsmrdbN9LC/dNmckfqODJiX9zjW2t3BIHYFeGBc6w9bK3Vre8A0Hg8yWkQpv6gaBLu+ykgi2DJQ==
dependencies:
"@atproto/common-web" "^0.2.3"
"@atproto/syntax@^0.3.0": "@atproto/syntax@^0.3.0":
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3" resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3"
integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA== integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==
"@atproto/xrpc-server@^0.4.2": "@atproto/xrpc-server@^0.5.1":
version "0.4.2" version "0.5.1"
resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.4.2.tgz#23efd89086b85933f1b0cc00c86e895adcaac315" resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.5.1.tgz#f63c86ba60bd5b9c5a641ea57191ff83d9db41fd"
integrity sha512-/m8rmFQFqFJ7WaVskPx27DLPeQfRCeEBMCdNxtyJZXElQZJMgcX5382SxAqsI3fVaW3EVwcQp0VuTNFOKFgHVg== integrity sha512-SXU6dscVe5iYxPeV79QIFs/yEEu7LLOzyHGoHG1kSNO6DjwxXTdcWOc8GSYGV6H+7VycOoPZPkyD9q4teJlj/w==
dependencies: dependencies:
"@atproto/common" "^0.3.3" "@atproto/common" "^0.4.0"
"@atproto/crypto" "^0.3.0" "@atproto/crypto" "^0.4.0"
"@atproto/lexicon" "^0.3.1" "@atproto/lexicon" "^0.4.0"
cbor-x "^1.5.1" cbor-x "^1.5.1"
express "^4.17.2" express "^4.17.2"
http-errors "^2.0.0" http-errors "^2.0.0"
@@ -381,14 +328,6 @@
ws "^8.12.0" ws "^8.12.0"
zod "^3.21.4" zod "^3.21.4"
"@atproto/xrpc@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.4.1.tgz#2fb7e81a159b019339bbcdcf4e7ce8dc4e83bef0"
integrity sha512-EMRGiu6oDvFL03Hk2rG/WCL3QK0GjZs9psH80JVf8z2nfdsGON6yn0hw3jvRB26CBXqi58U8Uicyq8Ej5pVTAA==
dependencies:
"@atproto/lexicon" "^0.3.1"
zod "^3.21.4"
"@atproto/xrpc@^0.5.0": "@atproto/xrpc@^0.5.0":
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.5.0.tgz#dacbfd8f7b13f0ab5bd56f8fdd4b460e132a6032" resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.5.0.tgz#dacbfd8f7b13f0ab5bd56f8fdd4b460e132a6032"
@@ -3727,6 +3666,18 @@
cborg "^1.6.0" cborg "^1.6.0"
multiformats "^9.5.4" multiformats "^9.5.4"
"@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"
"@isaacs/ttlcache@^1.4.1": "@isaacs/ttlcache@^1.4.1":
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2"
@@ -4489,6 +4440,11 @@
mkdirp "^1.0.4" mkdirp "^1.0.4"
rimraf "^3.0.2" rimraf "^3.0.2"
"@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==
"@pmmmwh/react-refresh-webpack-plugin@^0.5.11", "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": "@pmmmwh/react-refresh-webpack-plugin@^0.5.11", "@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
version "0.5.11" version "0.5.11"
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz#7c2268cedaa0644d677e8c4f377bc8fb304f714a" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz#7c2268cedaa0644d677e8c4f377bc8fb304f714a"
@@ -8772,6 +8728,15 @@ axios@^1.3.4:
form-data "^4.0.0" form-data "^4.0.0"
proxy-from-env "^1.1.0" proxy-from-env "^1.1.0"
axios@^1.6.7:
version "1.6.8"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66"
integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^3.1.1: axobject-query@^3.1.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
@@ -9110,13 +9075,13 @@ better-opn@~3.0.2:
dependencies: dependencies:
open "^8.0.4" open "^8.0.4"
better-sqlite3@^7.6.2: better-sqlite3@^9.4.0:
version "7.6.2" version "9.4.5"
resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-7.6.2.tgz#47cd8cad5b9573cace535f950ac321166bc31384" resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-9.4.5.tgz#1d3422443a9924637cb06cc3ccc941b2ae932c65"
integrity sha512-S5zIU1Hink2AH4xPsN0W43T1/AJ5jrPh7Oy07ocuW/AKYYY02GWzz9NH0nbSMn/gw6fDZ5jZ1QsHt1BXAwJ6Lg== integrity sha512-uFVyoyZR9BNcjSca+cp3MWCv6upAv+tbMC4SWM51NIMhoQOm4tjIkyxFO/ZsYdGAF61WJBgdzyJcz4OokJi0gQ==
dependencies: dependencies:
bindings "^1.5.0" bindings "^1.5.0"
prebuild-install "^7.1.0" prebuild-install "^7.1.1"
bfj@^7.0.2: bfj@^7.0.2:
version "7.0.2" version "7.0.2"
@@ -9598,7 +9563,7 @@ cborg@^1.6.0:
resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1" resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1"
integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug== integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==
chalk@5.3.0, chalk@^5.0.1: chalk@5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
@@ -10267,7 +10232,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
shebang-command "^1.2.0" shebang-command "^1.2.0"
which "^1.2.9" which "^1.2.9"
cross-spawn@^7.0.2, cross-spawn@^7.0.3: cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -12690,6 +12655,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
fontfaceobserver@^2.1.0: fontfaceobserver@^2.1.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz#5fb392116e75d5024b7ec8e4f2ce92106d1488c8" resolved "https://registry.yarnpkg.com/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz#5fb392116e75d5024b7ec8e4f2ce92106d1488c8"
@@ -12702,6 +12672,14 @@ for-each@^0.3.3:
dependencies: dependencies:
is-callable "^1.1.3" 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"
fork-ts-checker-webpack-plugin@^6.5.0: fork-ts-checker-webpack-plugin@^6.5.0:
version "6.5.3" version "6.5.3"
resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3"
@@ -12908,10 +12886,10 @@ get-port@^3.2.0:
resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc"
integrity sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg== integrity sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==
get-port@^6.1.2: get-port@^5.1.1:
version "6.1.2" version "5.1.1"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-6.1.2.tgz#c1228abb67ba0e17fb346da33b15187833b9c08a" resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw== integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
get-stream@^4.0.0: get-stream@^4.0.0:
version "4.1.0" version "4.1.0"
@@ -12986,6 +12964,17 @@ glob@7.1.6:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
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@^6.0.1: glob@^6.0.1:
version "6.0.4" version "6.0.4"
resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
@@ -14177,6 +14166,15 @@ iterator.prototype@^1.1.0:
has-tostringtag "^1.0.0" has-tostringtag "^1.0.0"
reflect.getprototypeof "^1.0.3" reflect.getprototypeof "^1.0.3"
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"
jake@^10.8.5: jake@^10.8.5:
version "10.8.7" version "10.8.7"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f"
@@ -15915,6 +15913,11 @@ lower-case@^2.0.2:
dependencies: dependencies:
tslib "^2.0.3" tslib "^2.0.3"
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@^4.0.1: lru-cache@^4.0.1:
version "4.1.5" version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
@@ -16439,6 +16442,13 @@ minimatch@^5.0.1:
dependencies: dependencies:
brace-expansion "^2.0.1" brace-expansion "^2.0.1"
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"
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
@@ -16477,6 +16487,11 @@ minipass@^5.0.0:
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
"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==
minizlib@^2.1.1: minizlib@^2.1.1:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
@@ -16573,11 +16588,6 @@ multipipe@^4.0.0:
duplexer2 "^0.1.2" duplexer2 "^0.1.2"
object-assign "^4.1.0" object-assign "^4.1.0"
murmurhash@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/murmurhash/-/murmurhash-2.0.1.tgz#4097720e08cf978872194ad84ea5be2dec9b610f"
integrity sha512-5vQEh3y+DG/lMPM0mCGPDnyV8chYg/g7rl6v3Gd8WMF9S429ox3Xk8qrk174kWhG767KQMqqxLD1WnGd77hiew==
mute-stream@0.0.8: mute-stream@0.0.8:
version "0.0.8" version "0.0.8"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
@@ -17387,6 +17397,14 @@ path-parse@^1.0.5, path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 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-to-regexp@0.1.7: path-to-regexp@0.1.7:
version "0.1.7" version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
@@ -20120,6 +20138,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
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==
simple-concat@^1.0.0: simple-concat@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
@@ -20518,7 +20541,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -20527,7 +20550,7 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
is-fullwidth-code-point "^3.0.0" is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1" strip-ansi "^6.0.1"
string-width@^5.0.0, string-width@^5.0.1: string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
@@ -20627,6 +20650,13 @@ stringify-object@^3.3.0:
is-obj "^1.0.1" is-obj "^1.0.1"
is-regexp "^1.0.0" is-regexp "^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@^5.0.0, strip-ansi@^5.2.0: strip-ansi@^5.0.0, strip-ansi@^5.2.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
@@ -20634,13 +20664,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0:
dependencies: dependencies:
ansi-regex "^4.1.0" ansi-regex "^4.1.0"
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: strip-ansi@^7.0.1:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -20713,6 +20736,11 @@ structured-headers@^0.4.1:
resolved "https://registry.yarnpkg.com/structured-headers/-/structured-headers-0.4.1.tgz#77abd9410622c6926261c09b9d16cf10592694d1" resolved "https://registry.yarnpkg.com/structured-headers/-/structured-headers-0.4.1.tgz#77abd9410622c6926261c09b9d16cf10592694d1"
integrity sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg== integrity sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==
structured-headers@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/structured-headers/-/structured-headers-1.0.1.tgz#1821e434e0fe45bdd78f07c779b16519ab520415"
integrity sha512-QYBxdBtA4Tl5rFPuqmbmdrS9kbtren74RTJTcs0VSQNVV5iRhJD4QlYTLD0+81SBwUQctjEQzjTRI3WG4DzICA==
style-loader@^3.3.1: style-loader@^3.3.1:
version "3.3.3" version "3.3.3"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.3.tgz#bba8daac19930169c0c9c96706749a597ae3acff" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.3.tgz#bba8daac19930169c0c9c96706749a597ae3acff"
@@ -22330,19 +22358,19 @@ workbox-window@6.6.1:
"@types/trusted-types" "^2.0.2" "@types/trusted-types" "^2.0.2"
workbox-core "6.6.1" workbox-core "6.6.1"
wrap-ansi@^6.2.0: "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "6.2.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies: dependencies:
ansi-styles "^4.0.0" ansi-styles "^4.0.0"
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0: wrap-ansi@^6.2.0:
version "7.0.0" version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
dependencies: dependencies:
ansi-styles "^4.0.0" ansi-styles "^4.0.0"
string-width "^4.1.0" string-width "^4.1.0"