diff --git a/.github/scripts/frame-slack-screenshots.mjs b/.github/scripts/frame-slack-screenshots.mjs new file mode 100644 index 0000000000..ef3c9db937 --- /dev/null +++ b/.github/scripts/frame-slack-screenshots.mjs @@ -0,0 +1,55 @@ +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' + +import sharp from 'sharp' + +export async function frameSlackScreenshots({ + inputPath, + outputPath, + outputDir, +}) { + const payload = JSON.parse(fs.readFileSync(inputPath, 'utf8')) + if (!Array.isArray(payload.file_uploads)) { + throw new Error('Slack upload payload must contain a file_uploads array') + } + + fs.mkdirSync(outputDir, {recursive: true}) + const framedUploads = [] + for (const [index, upload] of payload.file_uploads.entries()) { + if ( + typeof upload.file !== 'string' || + typeof upload.filename !== 'string' + ) { + throw new Error(`Invalid Slack file upload at index ${index}`) + } + const filename = `${path.parse(path.basename(upload.filename)).name}.png` + const framedFile = path.join(outputDir, filename) + await sharp(upload.file) + .resize(1600, 1200, {fit: 'contain', background: '#f8f8f8'}) + .png() + .toFile(framedFile) + framedUploads.push({ + ...upload, + file: framedFile, + filename, + highlight_type: 'png', + }) + } + + payload.file_uploads = framedUploads + fs.writeFileSync(outputPath, `${JSON.stringify(payload)}\n`) +} + +if ( + process.argv[1] && + path.resolve(process.argv[1]) === path.resolve(import.meta.filename) +) { + const [inputPath, outputPath, outputDir] = process.argv.slice(2) + if (!inputPath || !outputPath || !outputDir) { + throw new Error( + 'Usage: frame-slack-screenshots.mjs ', + ) + } + await frameSlackScreenshots({inputPath, outputPath, outputDir}) +} diff --git a/.github/scripts/frame-slack-screenshots.test.mjs b/.github/scripts/frame-slack-screenshots.test.mjs new file mode 100644 index 0000000000..9c018a7f7a --- /dev/null +++ b/.github/scripts/frame-slack-screenshots.test.mjs @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import test from 'node:test' + +import sharp from 'sharp' + +import {frameSlackScreenshots} from './frame-slack-screenshots.mjs' + +test('frames Slack screenshots as 4:3 PNGs', async t => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'maestro-slack-frame-')) + t.after(() => fs.rmSync(root, {recursive: true, force: true})) + const source = path.join(root, 'source.png') + const input = path.join(root, 'input.json') + const output = path.join(root, 'output.json') + const outputDir = path.join(root, 'images') + await sharp({ + create: { + width: 2, + height: 4, + channels: 3, + background: '#ffffff', + }, + }) + .png() + .toFile(source) + fs.writeFileSync( + input, + JSON.stringify({ + file_uploads: [ + { + file: source, + filename: '1-android-login.png', + alt_text: 'Android failure screenshot for login', + }, + ], + }), + ) + + await frameSlackScreenshots({inputPath: input, outputPath: output, outputDir}) + + const payload = JSON.parse(fs.readFileSync(output, 'utf8')) + const {data, info} = await sharp(payload.file_uploads[0].file) + .raw() + .toBuffer({resolveWithObject: true}) + const pixelAt = (x, y) => { + const offset = (y * info.width + x) * info.channels + return Array.from(data.subarray(offset, offset + 3)) + } + + assert.equal(info.width, 1600) + assert.equal(info.height, 1200) + assert.deepEqual(pixelAt(0, 0), [248, 248, 248]) + assert.deepEqual(pixelAt(800, 600), [255, 255, 255]) + assert.equal(payload.file_uploads[0].filename, '1-android-login.png') + assert.equal(payload.file_uploads[0].highlight_type, 'png') +}) diff --git a/.github/scripts/maestro-slack.mjs b/.github/scripts/maestro-slack.mjs new file mode 100644 index 0000000000..4c4c0dd27f --- /dev/null +++ b/.github/scripts/maestro-slack.mjs @@ -0,0 +1,330 @@ +import path from 'node:path' + +const CAROUSEL_LIMIT = 10 + +function concise(value, limit) { + return value.length > limit ? `${value.slice(0, limit - 1)}…` : value +} + +function slackEscape(value) { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') +} + +function mrkdwnText(value, limit) { + return concise(slackEscape(value), limit) +} + +function pluralize(count, singular) { + return `${count} ${singular}${count === 1 ? '' : 's'}` +} + +function stateFor(platforms) { + if (platforms.some(platform => platform.status === 'cancelled')) { + return 'cancelled' + } + if (platforms.some(platform => platform.failures.length > 0)) { + return 'failed' + } + if (platforms.some(platform => platform.failed)) { + return 'setup_failed' + } + return 'passed' +} + +function statePresentation(state, failureCount) { + if (state === 'cancelled') { + return { + header: '⏹️ Nightly Maestro E2E cancelled', + summary: 'results may be incomplete', + fallback: 'Nightly Maestro E2E was cancelled. Results may be incomplete.', + } + } + if (state === 'setup_failed') { + return { + header: '⚠️ Nightly Maestro setup failed', + summary: 'no complete test results', + fallback: 'Nightly Maestro E2E setup failed before tests could complete.', + } + } + if (state === 'failed') { + return { + header: '🚨 Nightly Maestro E2E failed', + summary: `${pluralize(failureCount, 'failed flow')}`, + fallback: `Nightly Maestro E2E failed with ${pluralize(failureCount, 'failed flow')}.`, + } + } + return { + header: '✅ Nightly Maestro E2E passed', + summary: 'all platforms passed', + fallback: 'Nightly Maestro E2E passed on all platforms.', + } +} + +function platformStatus(platform) { + if (platform.status === 'cancelled') return '⏹️ Cancelled' + if (platform.status === 'skipped') return '⏭️ Skipped' + if (platform.failures.length > 0) { + return `❌ Failed · ${pluralize(platform.failures.length, 'flow')}` + } + if (platform.failed && !platform.hasJUnit) return '⚠️ Setup failed' + if (platform.failed) return '❌ Failed' + return '✅ Passed' +} + +function selectFailures(platforms, limit = CAROUSEL_LIMIT) { + const queues = platforms.map(platform => + platform.failures.map(failure => ({platform, failure})), + ) + const selected = [] + + while (selected.length < limit && queues.some(queue => queue.length > 0)) { + for (const queue of queues) { + const next = queue.shift() + if (next) selected.push(next) + if (selected.length === limit) break + } + } + + return selected +} + +function uploadFilename({platform, failure}, index) { + const extension = path.extname(failure.screenshot) || '.png' + const slug = failure.name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + return `${index + 1}-${platform.name.toLowerCase()}-${slug || 'failed-flow'}${extension}` +} + +function buildUploadPayload(selectedFailures) { + return { + file_uploads: selectedFailures + .filter(({failure}) => failure.screenshot) + .map((entry, index) => ({ + file: entry.failure.screenshot, + filename: uploadFilename(entry, index), + highlight_type: 'png', + alt_text: `${entry.platform.name} failure screenshot for ${entry.failure.name}`, + })), + } +} + +function buildThreadPayload(platforms) { + const lines = ['*All Maestro failure details*'] + for (const platform of platforms) { + if (platform.failures.length === 0) continue + lines.push('', `*${slackEscape(platform.name)}*`) + for (const failure of platform.failures) { + lines.push( + `• *${mrkdwnText(failure.name, 140)}*\n ${mrkdwnText(failure.message, 300)}`, + ) + } + if (platform.artifactUrl) { + lines.push(`<${platform.artifactUrl}|Open ${platform.name} artifacts>`) + } + } + return {text: lines.join('\n')} +} + +function buildCarousel(selectedFailures, slackFileIds) { + let screenshotIndex = 0 + const elements = selectedFailures.map(({platform, failure}, index) => { + const slackFileId = failure.screenshot + ? slackFileIds[screenshotIndex++] + : undefined + return { + type: 'card', + block_id: `maestro_failure_${index + 1}`, + title: { + type: 'mrkdwn', + text: `*${mrkdwnText(failure.name, 140)}*`, + verbatim: true, + }, + subtitle: { + type: 'mrkdwn', + text: `${platform.name} · failed flow`, + verbatim: true, + }, + ...(slackFileId + ? { + hero_image: { + type: 'image', + slack_file: {id: slackFileId}, + alt_text: `${platform.name} failure screenshot for ${failure.name}`, + }, + } + : {}), + body: { + type: 'mrkdwn', + text: mrkdwnText(failure.message, 190), + verbatim: true, + }, + ...(platform.artifactUrl + ? { + subtext: { + type: 'mrkdwn', + text: `<${platform.artifactUrl}|Open logs and artifacts>`, + verbatim: true, + }, + } + : {}), + } + }) + + return {type: 'carousel', block_id: 'maestro_failures', elements} +} + +function diagnosticBlock(platform) { + const phase = mrkdwnText( + platform.phase || 'No phase metadata was captured', + 220, + ) + if (platform.status === 'cancelled') { + return { + type: 'section', + text: { + type: 'mrkdwn', + text: `*${platform.name} cancelled*\nLatest phase: ${phase}\nResults may be incomplete.`, + }, + } + } + if (!platform.hasJUnit) { + return { + type: 'section', + text: { + type: 'mrkdwn', + text: `*${platform.name} setup failed*\nLatest phase: ${phase}\nNo JUnit results were produced.`, + }, + } + } + return { + type: 'section', + text: { + type: 'mrkdwn', + text: `*${platform.name} job failed*\nLatest phase: ${phase}`, + }, + } +} + +function footerBlock(platforms, runUrl) { + const links = [`<${runUrl}|Open workflow>`] + for (const platform of platforms) { + if (platform.artifactUrl) { + links.push(`<${platform.artifactUrl}|${platform.name} artifacts>`) + } + } + return { + type: 'section', + text: {type: 'mrkdwn', text: links.join(' • ')}, + } +} + +export function buildSlackMessage({ + platforms, + sha, + runUrl, + commitUrl, + slackFileIds = [], +}) { + const state = stateFor(platforms) + const failureCount = platforms.reduce( + (total, platform) => total + platform.failures.length, + 0, + ) + const presentation = statePresentation(state, failureCount) + const allFailures = selectFailures(platforms, failureCount) + const selectedFailures = allFailures.slice(0, CAROUSEL_LIMIT) + const uploadPayload = buildUploadPayload(allFailures) + const detailBlocks = platforms + .filter( + platform => + platform.status === 'cancelled' || + (platform.failed && platform.failures.length === 0), + ) + .map(diagnosticBlock) + + if (selectedFailures.length > 0) { + detailBlocks.push(buildCarousel(selectedFailures, slackFileIds), { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `Showing ${selectedFailures.length} of ${pluralize(failureCount, 'failed flow')} • full details and screenshots are in the thread`, + }, + ], + }) + } + + const shortSha = sha.slice(0, 12) + const blocks = [ + { + type: 'header', + text: {type: 'plain_text', text: presentation.header}, + }, + { + type: 'context', + elements: [ + { + type: 'mrkdwn', + text: `Commit <${commitUrl}|\`${shortSha}\`> • ${presentation.summary}`, + }, + ], + }, + { + type: 'section', + fields: platforms.map(platform => ({ + type: 'mrkdwn', + text: `*${platform.name}*\n${platformStatus(platform)}`, + })), + }, + ...(detailBlocks.length > 0 ? [{type: 'divider'}, ...detailBlocks] : []), + footerBlock(platforms, runUrl), + ] + + return { + state, + failureCount, + screenshotCount: uploadPayload.file_uploads.length, + uploadPayload, + threadPayload: buildThreadPayload(platforms), + payload: {text: presentation.fallback, blocks}, + } +} + +export function extractSlackFileIds(response) { + if (!response) return [] + + let parsed = response + if (typeof response === 'string') { + try { + parsed = JSON.parse(response) + } catch { + return [] + } + } + + const ids = [] + const seen = new Set() + function visit(value) { + if (Array.isArray(value)) { + for (const item of value) visit(item) + return + } + if (!value || typeof value !== 'object') return + if ( + typeof value.id === 'string' && + /^F[A-Z0-9]+$/.test(value.id) && + !seen.has(value.id) + ) { + seen.add(value.id) + ids.push(value.id) + } + for (const child of Object.values(value)) visit(child) + } + visit(parsed) + return ids +} diff --git a/.github/scripts/maestro-slack.test.mjs b/.github/scripts/maestro-slack.test.mjs new file mode 100644 index 0000000000..afa052d61e --- /dev/null +++ b/.github/scripts/maestro-slack.test.mjs @@ -0,0 +1,194 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import {buildSlackMessage, extractSlackFileIds} from './maestro-slack.mjs' +import {screenshotsByFlow} from './summarize-maestro.mjs' + +function platform({ + name, + status = 'success', + failed = false, + failures = [], + phase = 'Completed', + hasJUnit = true, +}) { + return { + name, + status, + failed, + failures, + phase, + hasJUnit, + artifactUrl: `https://example.com/${name.toLowerCase()}`, + } +} + +function build(platforms, slackFileIds = []) { + return buildSlackMessage({ + platforms, + sha: '1234567890abcdef', + runUrl: 'https://example.com/run', + commitUrl: 'https://example.com/commit', + slackFileIds, + }) +} + +test('builds a screenshot carousel for failed flows', () => { + const platforms = [ + platform({ + name: 'iOS', + status: 'failure', + failed: true, + failures: [ + { + name: 'composer', + message: 'Element not found', + screenshot: '/tmp/screenshot-(composer).png', + }, + ], + }), + platform({name: 'Android'}), + ] + const summary = build(platforms, ['F123ABC']) + const carousel = summary.payload.blocks.find( + block => block.type === 'carousel', + ) + + assert.equal(summary.state, 'failed') + assert.equal(summary.screenshotCount, 1) + assert.equal( + summary.uploadPayload.file_uploads[0].file, + platforms[0].failures[0].screenshot, + ) + assert.equal(summary.uploadPayload.file_uploads[0].highlight_type, 'png') + assert.deepEqual(carousel.elements[0].hero_image.slack_file, {id: 'F123ABC'}) +}) + +test('selects failures across both platforms for the carousel', () => { + const failures = prefix => + Array.from({length: 7}, (_, index) => ({ + name: `${prefix}-${index}`, + message: 'Failed', + screenshot: `/tmp/${prefix}-${index}.png`, + })) + const summary = build([ + platform({ + name: 'iOS', + status: 'failure', + failed: true, + failures: failures('ios'), + }), + platform({ + name: 'Android', + status: 'failure', + failed: true, + failures: failures('android'), + }), + ]) + const carousel = summary.payload.blocks.find( + block => block.type === 'carousel', + ) + + assert.equal(carousel.elements.length, 10) + assert.equal(summary.failureCount, 14) + assert.equal(summary.screenshotCount, 14) + assert.equal(summary.uploadPayload.file_uploads.length, 14) + assert.match(summary.threadPayload.text, /ios-6/) + assert.match(summary.threadPayload.text, /android-6/) + assert.equal(carousel.elements[0].subtitle.text, 'iOS · failed flow') + assert.equal(carousel.elements[1].subtitle.text, 'Android · failed flow') +}) + +test('uses a cancellation presentation for partial results', () => { + const summary = build([ + platform({ + name: 'iOS', + status: 'cancelled', + failed: true, + failures: [], + phase: 'Building iOS development client', + hasJUnit: false, + }), + platform({ + name: 'Android', + status: 'cancelled', + failed: true, + failures: [], + phase: 'Building Android development client', + hasJUnit: false, + }), + ]) + + assert.equal(summary.state, 'cancelled') + assert.equal( + summary.payload.blocks[0].text.text, + '⏹️ Nightly Maestro E2E cancelled', + ) + assert.match(summary.payload.text, /cancelled/) + assert.equal(summary.screenshotCount, 0) +}) + +test('distinguishes setup failures from failed Maestro flows', () => { + const summary = build([ + platform({ + name: 'iOS', + status: 'failure', + failed: true, + failures: [], + phase: 'Starting Metro', + hasJUnit: false, + }), + platform({name: 'Android', status: 'skipped'}), + ]) + + assert.equal(summary.state, 'setup_failed') + assert.equal( + summary.payload.blocks[0].text.text, + '⚠️ Nightly Maestro setup failed', + ) + assert.equal( + summary.payload.blocks.some(block => block.type === 'carousel'), + false, + ) +}) + +test('extracts file ids from single and multi-file upload responses', () => { + const response = { + ok: true, + files: [ + { + ok: true, + files: [{id: 'FONE'}, {id: 'FTWO'}], + }, + ], + } + + assert.deepEqual(extractSlackFileIds(response), ['FONE', 'FTWO']) + assert.deepEqual( + extractSlackFileIds(JSON.stringify({ok: true, files: [{id: 'FTHREE'}]})), + ['FTHREE'], + ) + assert.deepEqual(extractSlackFileIds('not json'), []) +}) + +test('selects the newest Maestro screenshot for each flow', () => { + const screenshots = screenshotsByFlow([ + '/tmp/screenshot-❌-300-(composer).png', + '/tmp/screenshot-❌-100-(composer).png', + '/tmp/screenshot-❌-200-(login).png', + '/tmp/artifacts/maestro/composer-self-label/screenshots/step-020-tapOnElement-openMediaBtn.png', + '/tmp/artifacts/maestro/composer-self-label/screenshots/step-010-launchApp.png', + '/tmp/not-a-maestro-screenshot.png', + ]) + + assert.equal( + screenshots.get('composer'), + '/tmp/screenshot-❌-300-(composer).png', + ) + assert.equal(screenshots.get('login'), '/tmp/screenshot-❌-200-(login).png') + assert.equal( + screenshots.get('composer-self-label'), + '/tmp/artifacts/maestro/composer-self-label/screenshots/step-020-tapOnElement-openMediaBtn.png', + ) + assert.equal(screenshots.size, 3) +}) diff --git a/.github/scripts/summarize-maestro.mjs b/.github/scripts/summarize-maestro.mjs index 7154f5b0ee..375effa115 100644 --- a/.github/scripts/summarize-maestro.mjs +++ b/.github/scripts/summarize-maestro.mjs @@ -2,6 +2,8 @@ import fs from 'node:fs' import path from 'node:path' import process from 'node:process' +import {buildSlackMessage, extractSlackFileIds} from './maestro-slack.mjs' + const ENTITY_REPLACEMENTS = { '&': '&', ''': "'", @@ -101,6 +103,54 @@ function readPhase(root) { return phaseFile ? fs.readFileSync(phaseFile, 'utf8').trim() : '' } +function screenshotMetadata(file) { + const legacyMatch = path + .basename(file) + .match(/^screenshot-.*?-(\d+)-\((.+)\)\.(?:gif|jpe?g|png)$/i) + if (legacyMatch) { + return { + file, + order: Number(legacyMatch[1]), + flowName: legacyMatch[2], + } + } + + const parts = file.split(/[\\/]/) + const screenshotsIndex = parts.lastIndexOf('screenshots') + if ( + screenshotsIndex < 2 || + !parts.slice(0, screenshotsIndex - 1).includes('maestro') + ) { + return undefined + } + const step = parts.at(-1)?.match(/^step-(\d+)-.*\.(?:gif|jpe?g|png)$/i) + return step + ? { + file, + order: Number(step[1]), + flowName: parts[screenshotsIndex - 1], + } + : undefined +} + +export function screenshotsByFlow(files) { + const screenshots = new Map() + for (const file of files) { + const screenshot = screenshotMetadata(file) + if (!screenshot) continue + const current = screenshots.get(screenshot.flowName) + if (!current || screenshot.order > current.order) { + screenshots.set(screenshot.flowName, screenshot) + } + } + return new Map( + [...screenshots].map(([flowName, screenshot]) => [ + flowName, + screenshot.file, + ]), + ) +} + function platformResult({name, status, root, artifactUrl}) { const files = walk(root) const reports = files.filter(file => /(?:report|junit).*\.xml$/i.test(file)) @@ -115,7 +165,12 @@ function platformResult({name, status, root, artifactUrl}) { ) // A cancelled or timed-out Maestro run may never flush JUnit. Its CLI log is // streamed continuously, so use those failure lines when JUnit has no detail. - const failures = junitFailures.length > 0 ? junitFailures : cliFailures + const rawFailures = junitFailures.length > 0 ? junitFailures : cliFailures + const screenshots = screenshotsByFlow(files) + const failures = rawFailures.map(failure => ({ + ...failure, + screenshot: screenshots.get(failure.name), + })) // A skipped platform (e.g. iOS while temporarily disabled) is not a failure // as long as it produced no flow failures. const failed = @@ -131,52 +186,15 @@ function platformResult({name, status, root, artifactUrl}) { } } -function statusEmoji(status) { - if (status === 'success') return ':white_check_mark:' - if (status === 'skipped') return ':fast_forward:' - return ':x:' -} - -function slackEscape(value) { - return value - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') -} - -function platformBlock(platform) { +function githubSummary({state, platforms, shortSha, runUrl, commitUrl}) { + const outcome = + state === 'cancelled' + ? 'cancelled' + : state === 'passed' + ? 'passed' + : 'failed' const lines = [ - `${statusEmoji(platform.status)} *${platform.name}* — job status: \`${platform.status}\``, - ] - if (platform.failures.length > 0) { - for (const failure of platform.failures.slice(0, 8)) { - lines.push( - `• *${slackEscape(failure.name)}:* ${slackEscape(failure.message)}`, - ) - } - if (platform.failures.length > 8) { - lines.push(`• …and ${platform.failures.length - 8} more failed flows`) - } - } else if (platform.failed && !platform.hasJUnit) { - lines.push( - `• *Setup phase:* ${slackEscape(platform.phase || 'No phase metadata was captured')}`, - ) - } else if (platform.failed) { - lines.push( - `• Job failed after JUnit was written; latest phase: ${slackEscape(platform.phase || 'unknown')}`, - ) - } - if (platform.artifactUrl) { - lines.push( - `• <${platform.artifactUrl}|Open ${platform.name} logs and artifacts>`, - ) - } - return lines.join('\n').slice(0, 3000) -} - -function githubSummary({notify, platforms, shortSha, runUrl, commitUrl}) { - const lines = [ - `# Nightly Maestro E2E ${notify ? 'failed' : 'passed'}`, + `# Nightly Maestro E2E ${outcome}`, '', `- Commit: [\`${shortSha}\`](${commitUrl})`, `- Workflow run: [open run](${runUrl})`, @@ -235,6 +253,7 @@ export function buildSummary({ sha, runUrl, commitUrl, + slackFileIds = [], }) { const platforms = [ platformResult({ @@ -252,73 +271,29 @@ export function buildSummary({ ] const notify = platforms.some(platform => platform.failed) const shortSha = sha.slice(0, 12) - const lines = [ - ':rotating_light: *Nightly Maestro E2E failed*', - `*Commit:* <${commitUrl}|\`${shortSha}\`>`, - `*Workflow run:* <${runUrl}|open run>`, - '', - ] - - for (const platform of platforms) { - lines.push( - `${statusEmoji(platform.status)} *${platform.name}* — job status: \`${platform.status}\``, - ) - if (platform.failures.length > 0) { - for (const failure of platform.failures.slice(0, 10)) { - lines.push( - `• *${slackEscape(failure.name)}:* ${slackEscape(failure.message)}`, - ) - } - if (platform.failures.length > 10) { - lines.push(`• …and ${platform.failures.length - 10} more failed flows`) - } - } else if (platform.failed && !platform.hasJUnit) { - lines.push( - `• Setup phase: ${platform.phase || 'No phase metadata was captured'}`, - ) - } else if (platform.failed) { - lines.push( - `• The job failed after JUnit was written (latest phase: ${platform.phase || 'unknown'})`, - ) - } - if (platform.artifactUrl) { - lines.push( - `• <${platform.artifactUrl}|${platform.name} logs and artifacts>`, - ) - } - lines.push('') - } - - const text = lines.join('\n').trim() - const blocks = [ - { - type: 'header', - text: {type: 'plain_text', text: 'Nightly Maestro E2E failed'}, - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: `*Commit:* <${commitUrl}|\`${shortSha}\`>\n*Workflow run:* <${runUrl}|open run>`, - }, - }, - {type: 'divider'}, - ...platforms.flatMap((platform, index) => [ - {type: 'section', text: {type: 'mrkdwn', text: platformBlock(platform)}}, - ...(index < platforms.length - 1 ? [{type: 'divider'}] : []), - ]), - ] + const slack = buildSlackMessage({ + platforms, + sha, + runUrl, + commitUrl, + slackFileIds, + }) return { notify, + state: slack.state, platforms, githubSummary: githubSummary({ - notify, + state: slack.state, platforms, shortSha, runUrl, commitUrl, }), - payload: {text, blocks}, + failureCount: slack.failureCount, + screenshotCount: slack.screenshotCount, + uploadPayload: slack.uploadPayload, + threadPayload: slack.threadPayload, + payload: slack.payload, } } @@ -351,6 +326,7 @@ if ( sha: args.sha, runUrl: args['run-url'], commitUrl: args['commit-url'], + slackFileIds: extractSlackFileIds(args['slack-upload-response']), }) process.stdout.write(`${JSON.stringify(summary)}\n`) } diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index 6910133b6f..4f0a8b6418 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -88,7 +88,7 @@ jobs: with: expo-token: ${{ secrets.EXPO_TOKEN }} - - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 with: distribution: "temurin" java-version: "17" @@ -195,7 +195,7 @@ jobs: # bundletool needs a JRE. ubuntu-latest ships a default JDK, but pin it explicitly # like the build job so the toolchain is deterministic. - - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 with: distribution: "temurin" java-version: "17" diff --git a/.github/workflows/claude-mention.yml b/.github/workflows/claude-mention.yml index 9c06aa5886..8ef4d8ab72 100644 --- a/.github/workflows/claude-mention.yml +++ b/.github/workflows/claude-mention.yml @@ -60,7 +60,7 @@ jobs: fetch-depth: 1 - name: 🤖 Claude - uses: anthropics/claude-code-action@459ad358ae43fea66bfefd0a1f8d840b4b9791fb # v1.0.194 + uses: anthropics/claude-code-action@e5ad3c7725bc2459721893f88879fef9dbcf97b0 # v1.0.202 env: ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev with: diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 309ae4807d..86ff9cf71c 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -45,7 +45,7 @@ jobs: fetch-depth: 1 - name: 🤖 Claude review - uses: anthropics/claude-code-action@459ad358ae43fea66bfefd0a1f8d840b4b9791fb # v1.0.194 + uses: anthropics/claude-code-action@e5ad3c7725bc2459721893f88879fef9dbcf97b0 # v1.0.202 env: ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev with: diff --git a/.github/workflows/nightly-e2e.yml b/.github/workflows/nightly-e2e.yml index 33388edea4..2693d70962 100644 --- a/.github/workflows/nightly-e2e.yml +++ b/.github/workflows/nightly-e2e.yml @@ -15,7 +15,7 @@ concurrency: env: CI: "1" - MAESTRO_VERSION: "2.6.1" + MAESTRO_VERSION: "2.10.0" MAESTRO_DRIVER_STARTUP_TIMEOUT: "180000" MAESTRO_CLI_NO_ANALYTICS: "1" MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED: "true" @@ -51,7 +51,7 @@ jobs: expo-token: ${{ secrets.EXPO_TOKEN }} - name: ☕️ Set up Java 17 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 with: distribution: temurin java-version: "17" @@ -62,12 +62,12 @@ jobs: - name: 🔤 Compile translations uses: ./.github/actions/compile-i18n - - name: 📥 Install Maestro 2.6.1 + - name: 📥 Install Maestro 2.10.0 run: | echo "Installing Maestro" > artifacts/ios/phase.txt curl -fsSL -o "$RUNNER_TEMP/maestro.zip" \ "https://github.com/mobile-dev-inc/Maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip" - echo "3440825f514f537c6a96bcf5de995780c2a4a7f83a43208fdc95d4f1fecfad3b $RUNNER_TEMP/maestro.zip" \ + echo "29b675e10cc12080e445e9bfb2e2b4e4dfb9c0f2e30d5884120d258b5e1cd991 $RUNNER_TEMP/maestro.zip" \ | shasum -a 256 --check unzip -q "$RUNNER_TEMP/maestro.zip" -d "$RUNNER_TEMP" echo "$RUNNER_TEMP/maestro/bin" >> "$GITHUB_PATH" @@ -176,7 +176,7 @@ jobs: expo-token: ${{ secrets.EXPO_TOKEN }} - name: ☕️ Set up Java 17 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 with: distribution: temurin java-version: "17" @@ -187,12 +187,12 @@ jobs: - name: 🔤 Compile translations uses: ./.github/actions/compile-i18n - - name: 📥 Install Maestro 2.6.1 + - name: 📥 Install Maestro 2.10.0 run: | echo "Installing Maestro" > artifacts/android/phase.txt curl -fsSL -o "$RUNNER_TEMP/maestro.zip" \ "https://github.com/mobile-dev-inc/Maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip" - echo "3440825f514f537c6a96bcf5de995780c2a4a7f83a43208fdc95d4f1fecfad3b $RUNNER_TEMP/maestro.zip" \ + echo "29b675e10cc12080e445e9bfb2e2b4e4dfb9c0f2e30d5884120d258b5e1cd991 $RUNNER_TEMP/maestro.zip" \ | shasum -a 256 --check unzip -q "$RUNNER_TEMP/maestro.zip" -d "$RUNNER_TEMP" echo "$RUNNER_TEMP/maestro/bin" >> "$GITHUB_PATH" @@ -411,14 +411,158 @@ jobs: --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ --commit-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}" \ > e2e-summary.json - echo "notify=$(jq -r .notify e2e-summary.json)" >> "$GITHUB_OUTPUT" - echo "payload=$(jq -c .payload e2e-summary.json)" >> "$GITHUB_OUTPUT" + { + echo "notify=$(jq -r .notify e2e-summary.json)" + echo "failure_count=$(jq -r .failureCount e2e-summary.json)" + echo "screenshot_count=$(jq -r .screenshotCount e2e-summary.json)" + } >> "$GITHUB_OUTPUT" + jq .uploadPayload e2e-summary.json > slack-screenshot-upload.json jq -r .githubSummary e2e-summary.json >> "$GITHUB_STEP_SUMMARY" - - name: 🔔 Notify Slack of E2E failures + - name: 📦 Set up pnpm for Slack screenshot framing + if: >- + steps.summary.outputs.notify == 'true' && + steps.summary.outputs.screenshot_count != '0' + continue-on-error: true + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - name: 🔧 Set up Node for Slack screenshot framing + if: >- + steps.summary.outputs.notify == 'true' && + steps.summary.outputs.screenshot_count != '0' + continue-on-error: true + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: package.json + cache: pnpm + + - name: 📦 Install Slack screenshot framing dependencies + if: >- + steps.summary.outputs.notify == 'true' && + steps.summary.outputs.screenshot_count != '0' + continue-on-error: true + run: pnpm install --frozen-lockfile + + - name: 🖼️ Frame failure screenshots for Slack cards + id: frame_screenshots + if: >- + steps.summary.outputs.notify == 'true' && + steps.summary.outputs.screenshot_count != '0' + continue-on-error: true + run: | + node .github/scripts/frame-slack-screenshots.mjs \ + slack-screenshot-upload.json \ + slack-screenshot-upload-framed.json \ + slack-screenshots + + - name: 📝 Build Slack message + if: steps.summary.outputs.notify == 'true' + env: + ANDROID_STATUS: ${{ needs.android.result }} + IOS_STATUS: ${{ needs.ios.result }} + SLACK_CHANNEL_ID: ${{ secrets.E2E_FAILURES_SLACK_CHANNEL_ID }} + run: | + node .github/scripts/summarize-maestro.mjs \ + --ios-status "$IOS_STATUS" \ + --android-status "$ANDROID_STATUS" \ + --ios-root downloaded-artifacts/ios \ + --android-root downloaded-artifacts/android \ + --artifact-urls artifact-links.json \ + --sha "$GITHUB_SHA" \ + --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + --commit-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}" \ + > e2e-summary-for-slack.json + jq --arg channel "$SLACK_CHANNEL_ID" \ + '.payload + {channel: $channel, unfurl_links: false, unfurl_media: false}' \ + e2e-summary-for-slack.json > slack-message.json + + - name: 🔔 Notify Slack of E2E result + id: notify_slack if: steps.summary.outputs.notify == 'true' uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 with: - webhook: ${{ secrets.E2E_FAILURES_SLACK_WEBHOOK }} - webhook-type: incoming-webhook - payload: ${{ steps.summary.outputs.payload }} + method: chat.postMessage + token: ${{ secrets.E2E_FAILURES_SLACK_BOT_TOKEN }} + payload-file-path: slack-message.json + errors: true + + - name: 🧵 Prepare Slack thread payloads + if: >- + steps.summary.outputs.notify == 'true' && + steps.summary.outputs.failure_count != '0' + env: + FRAMING_OUTCOME: ${{ steps.frame_screenshots.outcome }} + SLACK_CHANNEL_ID: ${{ secrets.E2E_FAILURES_SLACK_CHANNEL_ID }} + SLACK_THREAD_TS: ${{ steps.notify_slack.outputs.ts }} + run: | + upload_payload=slack-screenshot-upload.json + if [ "$FRAMING_OUTCOME" = "success" ]; then + upload_payload=slack-screenshot-upload-framed.json + fi + jq --arg channel "$SLACK_CHANNEL_ID" --arg thread_ts "$SLACK_THREAD_TS" \ + '.threadPayload + {channel: $channel, thread_ts: $thread_ts, unfurl_links: false, unfurl_media: false}' \ + e2e-summary-for-slack.json > slack-thread-details.json + jq --arg channel_id "$SLACK_CHANNEL_ID" --arg thread_ts "$SLACK_THREAD_TS" \ + '. + {channel_id: $channel_id, thread_ts: $thread_ts}' \ + "$upload_payload" > slack-screenshot-upload-thread.json + + - name: 🧾 Post all failure details to Slack thread + if: >- + steps.summary.outputs.notify == 'true' && + steps.summary.outputs.failure_count != '0' + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.E2E_FAILURES_SLACK_BOT_TOKEN }} + payload-file-path: slack-thread-details.json + errors: true + + - name: 🖼️ Upload failure screenshots to Slack thread + id: upload_screenshots + if: >- + steps.summary.outputs.notify == 'true' && + steps.summary.outputs.screenshot_count != '0' + continue-on-error: true + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: files.uploadV2 + token: ${{ secrets.E2E_FAILURES_SLACK_BOT_TOKEN }} + payload-file-path: slack-screenshot-upload-thread.json + errors: true + + - name: ⏳ Wait for Slack to process failure screenshots + if: steps.upload_screenshots.outcome == 'success' + run: sleep 5 + + - name: 📝 Build Slack message with screenshots + if: steps.upload_screenshots.outcome == 'success' + env: + ANDROID_STATUS: ${{ needs.android.result }} + IOS_STATUS: ${{ needs.ios.result }} + SLACK_CHANNEL_ID: ${{ secrets.E2E_FAILURES_SLACK_CHANNEL_ID }} + SLACK_THREAD_TS: ${{ steps.notify_slack.outputs.ts }} + SLACK_UPLOAD_RESPONSE: ${{ steps.upload_screenshots.outputs.response }} + run: | + node .github/scripts/summarize-maestro.mjs \ + --ios-status "$IOS_STATUS" \ + --android-status "$ANDROID_STATUS" \ + --ios-root downloaded-artifacts/ios \ + --android-root downloaded-artifacts/android \ + --artifact-urls artifact-links.json \ + --sha "$GITHUB_SHA" \ + --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + --commit-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}" \ + --slack-upload-response "$SLACK_UPLOAD_RESPONSE" \ + > e2e-summary-with-slack-files.json + jq --arg channel "$SLACK_CHANNEL_ID" --arg ts "$SLACK_THREAD_TS" \ + '.payload + {channel: $channel, ts: $ts, unfurl_links: false, unfurl_media: false}' \ + e2e-summary-with-slack-files.json > slack-message-update.json + + - name: 🔄 Add screenshots to Slack message + if: steps.upload_screenshots.outcome == 'success' + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.update + token: ${{ secrets.E2E_FAILURES_SLACK_BOT_TOKEN }} + payload-file-path: slack-message-update.json + errors: true diff --git a/.github/workflows/nightly-update-source-languages.yaml b/.github/workflows/nightly-update-source-languages.yaml index 2c8a68d35b..82fb3de3aa 100644 --- a/.github/workflows/nightly-update-source-languages.yaml +++ b/.github/workflows/nightly-update-source-languages.yaml @@ -35,7 +35,7 @@ jobs: commit_message: Nightly source-language update file_pattern: ./src/locale/locales/en/messages.po - name: 🚀 Push source lang to Crowdin - uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0 + uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1 with: upload_sources: true upload_sources_args: "-b main" diff --git a/__e2e__/flows/composer-self-label.yml b/__e2e__/flows/composer-self-label.yml index 4539e1e04e..065304a508 100644 --- a/__e2e__/flows/composer-self-label.yml +++ b/__e2e__/flows/composer-self-label.yml @@ -30,6 +30,10 @@ appId: xyz.blueskyweb.app id: "confirmBtn" - tapOn: id: "composerPublishBtn" +- extendedWaitUntil: + notVisible: + id: "composePostView" + timeout: 30000 - tapOn: id: "e2eRefreshHome" - assertVisible: "Adult Content" diff --git a/__e2e__/flows/onboarding-avatar-creator.yml b/__e2e__/flows/onboarding-avatar-creator.yml index 7b65c405bb..cb65d5bf44 100644 --- a/__e2e__/flows/onboarding-avatar-creator.yml +++ b/__e2e__/flows/onboarding-avatar-creator.yml @@ -60,6 +60,7 @@ appId: xyz.blueskyweb.app - tapOn: id: "onboardingContinue" - assertVisible: "What are your interests?" +- tapOn: "Animals" - tapOn: id: "onboardingContinue" - assertVisible: "Suggested for you" diff --git a/__e2e__/flows/onboarding.yml b/__e2e__/flows/onboarding.yml index 4c0ffdd48f..42ad4f87c2 100644 --- a/__e2e__/flows/onboarding.yml +++ b/__e2e__/flows/onboarding.yml @@ -41,6 +41,7 @@ appId: xyz.blueskyweb.app - tapOn: id: "onboardingContinue" - assertVisible: "What are your interests?" +- tapOn: "Animals" - tapOn: id: "onboardingContinue" - assertVisible: "Suggested for you" diff --git a/__e2e__/setupApp.yml b/__e2e__/setupApp.yml index ab9623ffba..be1794c523 100644 --- a/__e2e__/setupApp.yml +++ b/__e2e__/setupApp.yml @@ -3,34 +3,22 @@ appId: xyz.blueskyweb.app - launchApp: appId: "xyz.blueskyweb.app" clearState: true - arguments: - "-EXDevMenuIsOnboardingFinished": true -- runFlow: - when: - platform: iOS - commands: - - extendedWaitUntil: - visible: "http://localhost:8081" - timeout: 60000 - - tapOn: "http://localhost:8081" -- runFlow: - when: - platform: Android - commands: - - extendedWaitUntil: - visible: "http://10.0.2.2:8081" - timeout: 60000 - - tapOn: "http://10.0.2.2:8081" - - extendedWaitUntil: - visible: "Continue" - timeout: 180000 - - tapOn: "Continue" - - back - extendedWaitUntil: visible: id: e2eProxyHeaderInput timeout: 180000 -- tapOn: - id: e2eProxyHeaderInput -- inputText: ${output.result} -- pressKey: Enter +- extendedWaitUntil: + visible: "Sign in" + timeout: 180000 +- retry: + maxRetries: 3 + commands: + - tapOn: + id: e2eProxyHeaderInput + - eraseText + - inputText: ${output.result} + - pressKey: Enter + - extendedWaitUntil: + visible: + id: e2eSignInAlice + timeout: 10000 diff --git a/__tests__/lib/images.test.ts b/__tests__/lib/images.test.ts index f0f948365c..0ba18ecc0e 100644 --- a/__tests__/lib/images.test.ts +++ b/__tests__/lib/images.test.ts @@ -1,5 +1,9 @@ -import {createDownloadResumable, deleteAsync} from 'expo-file-system/legacy' -import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' +import { + createDownloadResumable, + deleteAsync, + getInfoAsync, +} from 'expo-file-system/legacy' +import {ImageManipulator, SaveFormat} from 'expo-image-manipulator' import {IMAGE_SIZE_CONFIG_2K_1MB} from '../../src/lib/constants' import { @@ -9,7 +13,6 @@ import { import {getResizedDimensions} from '../../src/lib/media/util' const mockResizedImage = { - path: 'file://resized-image.jpg', size: 100, width: 100, height: 100, @@ -20,10 +23,26 @@ describe('downloadAndResize', () => { const errorSpy = jest.spyOn(global.console, 'error') beforeEach(() => { - const mockedCreateResizedImage = manipulateAsync as jest.Mock - mockedCreateResizedImage.mockResolvedValue({ - uri: 'file://resized-image.jpg', - ...mockResizedImage, + let savedImageCount = 0 + const mockedManipulate = ImageManipulator.manipulate as jest.Mock + mockedManipulate.mockImplementation(() => { + const image = { + ...mockResizedImage, + release: jest.fn(), + uri: 'file://rendered-image.jpg', + saveAsync: jest.fn().mockImplementation(() => { + savedImageCount += 1 + return Promise.resolve({ + uri: `file://resized-image-${savedImageCount}.jpg`, + ...mockResizedImage, + }) + }), + } + return { + release: jest.fn(), + renderAsync: jest.fn().mockResolvedValue(image), + resize: jest.fn(), + } }) }) @@ -48,7 +67,10 @@ describe('downloadAndResize', () => { } const result = await downloadAndResize(opts) - expect(result).toEqual(mockResizedImage) + expect(result).toEqual({ + ...mockResizedImage, + path: 'file://resized-image-7.jpg', + }) expect(createDownloadResumable).toHaveBeenCalledWith( opts.uri, expect.anything(), @@ -57,20 +79,98 @@ describe('downloadAndResize', () => { }, ) - // First time it gets called is to get dimensions - expect(manipulateAsync).toHaveBeenCalledWith(expect.any(String), [], {}) + // First time it gets called is to get dimensions. + expect(ImageManipulator.manipulate).toHaveBeenNthCalledWith( + 1, + expect.any(String), + ) + const firstContext = (ImageManipulator.manipulate as jest.Mock).mock + .results[0].value + expect(firstContext.resize).not.toHaveBeenCalled() + // The mocked source image is 100x100, below maxDimension, so it is not // downsized. - expect(manipulateAsync).toHaveBeenCalledWith( - expect.any(String), - [{resize: {height: 100, width: 100}}], - {format: SaveFormat.JPEG, compress: 1.0}, + const secondContext = (ImageManipulator.manipulate as jest.Mock).mock + .results[1].value + expect(secondContext.resize).toHaveBeenCalledWith({ + height: 100, + width: 100, + }) + const lastContext = ( + ImageManipulator.manipulate as jest.Mock + ).mock.results.at(-1)!.value + const resizedImage = await lastContext.renderAsync.mock.results[0].value + expect(resizedImage.saveAsync).toHaveBeenCalledWith( + expect.objectContaining({format: SaveFormat.JPEG, compress: 1.0}), ) - expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), { + const deletedPaths = (deleteAsync as jest.Mock).mock.calls.map( + ([path]) => path, + ) + expect(deletedPaths).toEqual( + expect.arrayContaining([ + 'file://resized-image-1.jpg', + 'file://resized-image-2.jpg', + 'file://resized-image-3.jpg', + 'file://resized-image-4.jpg', + 'file://resized-image-5.jpg', + 'file://resized-image-6.jpg', + ]), + ) + expect(deletedPaths).not.toContain('file://resized-image-7.jpg') + }) + + it('deletes a partial download when downloading fails', async () => { + const mockedFetch = createDownloadResumable as jest.Mock + mockedFetch.mockReturnValue({ + cancelAsync: jest.fn(), + downloadAsync: jest.fn().mockRejectedValue(new Error('download failed')), + }) + + const opts: DownloadAndResizeOpts = { + uri: 'https://example.com/image.jpg', + maxDimension: 2000, + maxSize: 500000, + timeout: 10000, + } + + await expect(downloadAndResize(opts)).rejects.toThrow('download failed') + expect(deleteAsync).toHaveBeenCalledWith(expect.stringMatching(/\.bin$/), { idempotent: true, }) }) + it('deletes every intermediate image when resizing fails', async () => { + const mockedFetch = createDownloadResumable as jest.Mock + mockedFetch.mockReturnValue({ + cancelAsync: jest.fn(), + downloadAsync: jest + .fn() + .mockResolvedValue({uri: 'file://downloaded-image.jpg'}), + }) + ;(getInfoAsync as jest.Mock) + .mockResolvedValueOnce({exists: true, size: 100}) + .mockRejectedValueOnce(new Error('stat failed')) + + const opts: DownloadAndResizeOpts = { + uri: 'https://example.com/image.jpg', + maxDimension: 2000, + maxSize: 500000, + timeout: 10000, + } + + await expect(downloadAndResize(opts)).rejects.toThrow('stat failed') + const deletedPaths = (deleteAsync as jest.Mock).mock.calls.map( + ([path]) => path, + ) + expect(deletedPaths).toEqual( + expect.arrayContaining([ + 'file://resized-image-1.jpg', + 'file://resized-image-2.jpg', + 'file://resized-image-3.jpg', + ]), + ) + }) + it('should return undefined for invalid URI', async () => { const opts: DownloadAndResizeOpts = { uri: 'invalid-uri', diff --git a/__tests__/lib/link-meta.test.ts b/__tests__/lib/link-meta.test.ts index bcd3acff47..529935436d 100644 --- a/__tests__/lib/link-meta.test.ts +++ b/__tests__/lib/link-meta.test.ts @@ -1,13 +1,58 @@ -import {getLikelyType, LikelyType} from '../../src/lib/link-meta/link-meta' +import { + getLikelyType, + getLinkMeta, + LikelyType, +} from '../../src/lib/link-meta/link-meta' describe('getLikelyType', () => { - it('correctly handles non-parsed url', async () => { - const output = await getLikelyType('https://example.com') + it('correctly handles non-parsed url', () => { + const output = getLikelyType('https://example.com') expect(output).toEqual(LikelyType.HTML) }) - it('handles non-string urls without crashing', async () => { - const output = await getLikelyType('123') + it('handles non-string urls without crashing', () => { + const output = getLikelyType('123') expect(output).toEqual(LikelyType.Other) }) }) + +describe('getLinkMeta', () => { + const originalFetch = global.fetch + + afterEach(() => { + global.fetch = originalFetch + }) + + it('fetches metadata for stream.place routes that look like files', async () => { + const fetchMock = jest.fn().mockResolvedValue({ + json: () => + Promise.resolve({ + error: '', + description: 'AT Protocol livestreams', + image: 'https://stream.place/thumbnail.jpg', + title: 'atproto.com on stream.place', + }), + }) + global.fetch = fetchMock + + const output = await getLinkMeta('https://stream.place/atproto.com') + + expect(fetchMock).toHaveBeenCalledTimes(1) + expect(output).toMatchObject({ + description: 'AT Protocol livestreams', + image: 'https://stream.place/thumbnail.jpg', + likelyType: LikelyType.HTML, + title: 'atproto.com on stream.place', + }) + }) + + it('skips metadata fetching for direct image URLs', async () => { + const fetchMock = jest.fn() + global.fetch = fetchMock + + const output = await getLinkMeta('https://example.com/image.JPEG') + + expect(fetchMock).not.toHaveBeenCalled() + expect(output).toMatchObject({likelyType: LikelyType.Image}) + }) +}) diff --git a/app.config.js b/app.config.js index 22787090bb..bc1de0b2d7 100644 --- a/app.config.js +++ b/app.config.js @@ -20,6 +20,7 @@ module.exports = function (_config) { const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight' const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production' + const IS_E2E = process.env.EXPO_PUBLIC_ENV === 'e2e' const IS_DEV = !IS_TESTFLIGHT && !IS_PRODUCTION const ASSOCIATED_DOMAINS = [ @@ -234,6 +235,25 @@ module.exports = function (_config) { checkAutomatically: 'NEVER', }, plugins: [ + [ + 'expo-dev-client', + { + toolsButton: false, + ...(IS_E2E + ? { + launchMode: 'most-recent', + skipOnboarding: true, + showMenuAtLaunch: false, + ios: { + defaultLaunchURL: 'http://localhost:8081', + }, + android: { + defaultLaunchURL: 'http://10.0.2.2:8081', + }, + } + : {}), + }, + ], 'expo-video', 'expo-localization', 'expo-web-browser', @@ -271,8 +291,8 @@ module.exports = function (_config) { }, android: { compileSdkVersion: 36, - targetSdkVersion: 35, - buildToolsVersion: '35.0.0', + targetSdkVersion: 36, + buildToolsVersion: '36.0.0', buildReactNativeFromSource: IS_PRODUCTION, }, }, @@ -285,7 +305,6 @@ module.exports = function (_config) { sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'], }, ], - 'react-native-compressor', [ '@bitdrift/react-native', { diff --git a/assets/icons/exclamationCircle_stroke2_corner0_rounded.svg b/assets/icons/exclamationCircle_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..e56e521960 --- /dev/null +++ b/assets/icons/exclamationCircle_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/jest/jestSetup.js b/jest/jestSetup.js index 44a2c985f5..562c1063c7 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -36,15 +36,36 @@ jest.mock('expo-file-system/legacy', () => ({ createDownloadResumable: jest.fn(), })) -jest.mock('expo-image-manipulator', () => ({ - manipulateAsync: jest.fn().mockResolvedValue({ - uri: 'file://resized-image', - }), - SaveFormat: { - JPEG: 'jpeg', - WEBP: 'webp', - }, -})) +jest.mock('expo-image-manipulator', () => { + const createContext = () => { + const image = { + height: 100, + release: jest.fn(), + saveAsync: jest.fn().mockResolvedValue({ + height: 100, + uri: 'file://resized-image', + width: 100, + }), + width: 100, + } + return { + crop: jest.fn(), + release: jest.fn(), + renderAsync: jest.fn().mockResolvedValue(image), + resize: jest.fn(), + } + } + + return { + ImageManipulator: { + manipulate: jest.fn(createContext), + }, + SaveFormat: { + JPEG: 'jpeg', + WEBP: 'webp', + }, + } +}) jest.mock('expo-camera', () => ({ Camera: { diff --git a/lexicons.json b/lexicons.json index ca0e64f570..1d8d9d5dab 100644 --- a/lexicons.json +++ b/lexicons.json @@ -92,6 +92,7 @@ "app.bsky.graph.muteActor", "app.bsky.graph.muteActorList", "app.bsky.graph.muteThread", + "app.bsky.graph.referencelistoptout", "app.bsky.graph.searchStarterPacks", "app.bsky.graph.searchStarterPacksV2", "app.bsky.graph.starterpack", @@ -426,7 +427,7 @@ }, "app.bsky.embed.video": { "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.video", - "cid": "bafyreiaqos23yv3t4ptrxily6s6qea5fcxfjzjlm42zq46xweby2mkgr4m" + "cid": "bafyreihoxb7lvczityqcv2s5od3rllmkee7tn3m4qg6wn34kd6m45p6v24" }, "app.bsky.feed.defs": { "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.defs", @@ -538,7 +539,7 @@ }, "app.bsky.graph.defs": { "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.defs", - "cid": "bafyreifcipomli7yggtl46xufgxlnrw7se6xmsdxmzgfcz2tiu76ljatxm" + "cid": "bafyreief2f7zpllyicjugbn7faohmnzwujeiytfzj76uckxmrqmtdvechy" }, "app.bsky.graph.follow": { "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.follow", @@ -632,6 +633,10 @@ "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.muteThread", "cid": "bafyreib6ppci3qzye6wktkm4byxtb5mnl2vg22fm7oawcdvof2tfogx4dy" }, + "app.bsky.graph.referencelistoptout": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.referencelistoptout", + "cid": "bafyreifode2cfu7x7yamiorzg66u46l4zdr2yxmtuoikrup7j2dhwzxf3q" + }, "app.bsky.graph.searchStarterPacks": { "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.searchStarterPacks", "cid": "bafyreia446ip6mbnwpml6hlvxab7jtsud7zczde3u6zmnsa3op4bpxu7um" diff --git a/lexicons/app/bsky/embed/video.json b/lexicons/app/bsky/embed/video.json index 5033261982..9a7df6c796 100644 --- a/lexicons/app/bsky/embed/video.json +++ b/lexicons/app/bsky/embed/video.json @@ -9,9 +9,7 @@ "properties": { "alt": { "type": "string", - "maxLength": 10000, - "description": "Alt text description of the video, for accessibility.", - "maxGraphemes": 1000 + "description": "Alt text description of the video, for accessibility." }, "video": { "type": "blob", @@ -51,9 +49,7 @@ ], "properties": { "alt": { - "type": "string", - "maxLength": 10000, - "maxGraphemes": 1000 + "type": "string" }, "cid": { "type": "string", diff --git a/lexicons/app/bsky/feed/defs.json b/lexicons/app/bsky/feed/defs.json index a59b32914d..97cb057f6f 100644 --- a/lexicons/app/bsky/feed/defs.json +++ b/lexicons/app/bsky/feed/defs.json @@ -169,6 +169,28 @@ } } }, + "knownLikers": { + "type": "object", + "required": [ + "count", + "actors" + ], + "properties": { + "count": { + "type": "integer" + }, + "actors": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "maxLength": 5, + "minLength": 0 + } + }, + "description": "The post's likers whom you also follow" + }, "requestLess": { "type": "token", "description": "Request that less content like the given feed item be shown in the feed" @@ -194,6 +216,11 @@ "bookmarked": { "type": "boolean" }, + "knownLikers": { + "ref": "#knownLikers", + "type": "ref", + "description": "This property is present only in selected cases, as an optimization." + }, "threadMuted": { "type": "boolean" }, @@ -202,37 +229,10 @@ }, "embeddingDisabled": { "type": "boolean" - }, - "knownLikers": { - "description": "This property is present only in selected cases, as an optimization.", - "type": "ref", - "ref": "#knownLikers" } }, "description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests." }, - "knownLikers": { - "type": "object", - "description": "The post's likers whom you also follow", - "required": [ - "count", - "actors" - ], - "properties": { - "count": { - "type": "integer" - }, - "actors": { - "type": "array", - "minLength": 0, - "maxLength": 5, - "items": { - "type": "ref", - "ref": "app.bsky.actor.defs#profileViewBasic" - } - } - } - }, "feedViewPost": { "type": "object", "required": [ diff --git a/lexicons/app/bsky/graph/defs.json b/lexicons/app/bsky/graph/defs.json index eca42fc875..8d77562184 100644 --- a/lexicons/app/bsky/graph/defs.json +++ b/lexicons/app/bsky/graph/defs.json @@ -100,6 +100,11 @@ "subject": { "ref": "app.bsky.actor.defs#profileView", "type": "ref" + }, + "subjectOptedOut": { + "type": "boolean", + "const": true, + "description": "Set to true when the subject has opted out of appearing in the reference list. Only set when the viewer owns the list." } } }, @@ -228,6 +233,11 @@ "blocked": { "type": "string", "format": "at-uri" + }, + "referenceListOptOut": { + "type": "string", + "format": "at-uri", + "description": "The authenticated viewer's app.bsky.graph.referencelistoptout record URI for this reference list. Only set for reference lists. A client can delete this record to undo the opt-out." } } }, diff --git a/lexicons/app/bsky/graph/referencelistoptout.json b/lexicons/app/bsky/graph/referencelistoptout.json new file mode 100644 index 0000000000..e5387d8d82 --- /dev/null +++ b/lexicons/app/bsky/graph/referencelistoptout.json @@ -0,0 +1,30 @@ +{ + "id": "app.bsky.graph.referencelistoptout", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "subject": { + "type": "string", + "format": "at-uri", + "description": "Canonical, DID-based AT URI of the app.bsky.graph.list record from which the author requests omission." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + }, + "description": "Record requesting that its author be omitted from the public presentation of a reference list. This record is only enforced when the subject list's current purpose is app.bsky.graph.defs#referencelist. AppView indexes at most one record per actor and list pair, and ignores duplicate records." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/modules/bottom-sheet/README.md b/modules/bottom-sheet/README.md index 49007d97b5..c3653a0298 100644 --- a/modules/bottom-sheet/README.md +++ b/modules/bottom-sheet/README.md @@ -61,7 +61,8 @@ The component uses a class-based approach to expose imperative methods (`present - Preserves status/nav bar appearance from host activity - **DialogRootViewGroup.kt**: Custom ViewGroup acting as RootView for the dialog - Forwards touch events to React Native event system - - Updates shadow node size to match window dimensions + - Reports its measured width to `BottomSheetView` so the content canvas can follow it + - Also carries the legacy `UIManagerModule.updateNodeSize()` shadow node sizing, which only runs on the old architecture - Based on React Native's ReactModalHostView pattern - **SheetManager.kt**: Singleton for tracking sheets (same pattern as iOS) @@ -74,6 +75,24 @@ Both platforms detect content height changes natively without JS bridge round-tr This eliminates layout jank when content changes (e.g., keyboard appearance, dynamic content loading). +### Content Canvas Sizing + +The "canvas" is the size the sheet content is laid out on by Yoga. **On Android the native side owns it**; on iOS it is still sized from JS. + +- **Android**: JS renders unsized `flex: 1` content and `BottomSheetView` pushes the canvas size into the Fabric shadow tree through `ExpoView`'s `setViewSize` state channel (`shadowNodeProxy.setViewSize()`). Only native knows the real sheet frame - Material caps the frame at 640dp on tablets and centers it, and it changes on rotation. +- **iOS**: `BottomSheetNativeComponent` sets `height: screenHeight - insets.top` and `width: '100%'` on the native view. Moving iOS onto the same state channel is deferred: it needs on-device iteration on iOS 26 sheet geometry (large-detent and floating-card metrics, where the visible sheet is shorter than the window minus the top inset). + +How the Android path works: + +- The JS style on the native view **must not set `width` or `height`** on Android. `ExpoViewComponentDescriptor::adopt()` only applies the state size on an axis where the style leaves that dimension undefined, so a style dimension would silently win. +- The two axes come from different places, and the distinction is load-bearing: + - **Width** is authoritatively the dialog container's measured width, reported through `DialogRootViewGroup`'s size-change listener - that is the real sheet width, with the horizontal window insets and Material's 640dp cap already applied. It is seeded from `min(window width, material_bottom_sheet_max_width)` on the first `onLayout` so content has something to lay out in before the dialog exists. + - **Height** is always computed natively as `screenHeight - statusBarHeight` (matching the behavior's `expandedOffset`) - the whole expanded frame, **never** the dialog's measured height. The canvas has to be room for the content to grow *into*, because the content's height is what drives the snap points. Sizing it from the dialog's own height is circular: `BottomSheetBehavior` measures the container against the sheet, so the canvas collapses onto the content height and the content is then pinned - extra `ScrollView` padding (the Android keyboard path) or a longer list becomes scroll extent instead of a height change, `OnLayoutChangeListener` never fires, and the sheet stops responding to its content. +- Seeding runs once per open cycle - re-seeding would fight the width the dialog reported and the two would push each other back and forth. +- Because the content measures 0x0 until that first state commit lands, `present()` bails out early when the content height is still zero. The commit resizes the native view, which re-fires `onLayout`, which re-enters `present()` - so presentation self-retries rather than needing an explicit callback. Full-height sheets skip the check, since they don't need a content measurement. +- Rotation is handled by the container push: the RN activity handles configuration changes itself, so the view is never recreated. `screenHeight` is read per access so the computed height follows the rotation, and the container reports the new width (plus a deferred `updateLayout()` to reposition the sheet). +- On the **old architecture** there is no state channel (`stateWrapper` is null, so `setViewSize` no-ops) and Android falls back to `DialogRootViewGroup`'s legacy `UIManagerModule.updateNodeSize()` path. The `present()` gate is skipped there for the same reason - nothing would ever resize the view. + ## Props ```typescript @@ -213,6 +232,10 @@ BottomSheetNativeComponent.dismissAll() 4. **Layout Updates During Gestures**: Content height changes are deferred during drag gestures to prevent fighting the user's input. +5. **Tablet Width**: Material caps the sheet frame at 640dp (`material_bottom_sheet_max_width`, the `android:maxWidth` on `Widget.MaterialComponents.BottomSheet`) and centers it horizontally, so on tablets the sheet is narrower than the screen. `BottomSheetView` reads that cap from resources when seeding the canvas width, and the dialog container's measured width then corrects it - see [Content Canvas Sizing](#content-canvas-sizing). + +6. **Rotation**: The RN activity handles configuration changes itself, so a rotation resizes the display without recreating `BottomSheetView`. Screen height is therefore read per access rather than cached, and `maxHeight` is stored unclamped and clamped against the current screen at use time. + ### Platform Differences - **cornerRadius**: Applied to sheet on iOS, to content wrapper on Android (Android clips with `overflow: hidden`) diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt index 1b9224c6e7..38a1437247 100644 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt @@ -21,6 +21,12 @@ import expo.modules.kotlin.AppContext import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.views.ExpoView +/** + * Fallback for Material's `material_bottom_sheet_max_width` dimen (in dp), used only + * if the resource lookup fails. 640dp is the value Material ships. + */ +private const val FALLBACK_MAX_SHEET_WIDTH_DP = 640f + class BottomSheetView( context: Context, appContext: AppContext, @@ -38,26 +44,30 @@ class BottomSheetView( private var lastObservedContentHeight: Float = 0f private var pendingLayoutUpdate: Boolean = false - private val screenHeight: Float = - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) { - // API 35+: edge-to-edge is mandatory, heightPixels is the full display - context.resources.displayMetrics.heightPixels - .toFloat() - } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { - // API 30-34: heightPixels may exclude nav bar, use currentWindowMetrics - val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager - wm.currentWindowMetrics.bounds - .height() - .toFloat() - } else { - // API < 30: currentWindowMetrics not available, use getRealSize - // which includes system bars (heightPixels may exclude them) - val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager - val size = android.graphics.Point() - @Suppress("DEPRECATION") - wm.defaultDisplay.getRealSize(size) - size.y.toFloat() - } + // Computed per read rather than cached at construction: the RN activity handles + // configuration changes itself, so a rotation resizes the display without + // recreating this view and a cached value would stay stale for the sheet's life. + private val screenHeight: Float + get() = + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) { + // API 35+: edge-to-edge is mandatory, heightPixels is the full display + context.resources.displayMetrics.heightPixels + .toFloat() + } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + // API 30-34: heightPixels may exclude nav bar, use currentWindowMetrics + val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager + wm.currentWindowMetrics.bounds + .height() + .toFloat() + } else { + // API < 30: currentWindowMetrics not available, use getRealSize + // which includes system bars (heightPixels may exclude them) + val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager + val size = android.graphics.Point() + @Suppress("DEPRECATION") + wm.defaultDisplay.getRealSize(size) + size.y.toFloat() + } private fun getNavigationBarHeight(): Int { val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") @@ -73,6 +83,11 @@ class BottomSheetView( private val onSnapPointChange by EventDispatcher() private val onStateChange by EventDispatcher() + // Last canvas size (in dp) pushed into the shadow tree, so repeated layout + // passes don't spam state updates + private var lastPushedCanvasWidth: Float = -1f + private var lastPushedCanvasHeight: Float = -1f + var disableDrag = false set(value) { field = value @@ -99,10 +114,11 @@ class BottomSheetView( field = if (value < 0) 0f else dpToPx(value) } - var maxHeight = this.screenHeight + // Stored unclamped (in px) because screenHeight can change under us on rotation. + // The clamp against the screen happens at use time, in getTargetHeight(). + var maxHeight = Float.MAX_VALUE set(value) { - val px = dpToPx(value) - field = if (px > this.screenHeight) this.screenHeight else px + field = dpToPx(value) } private var isOpen: Boolean = false @@ -140,6 +156,38 @@ class BottomSheetView( this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id) this.dialogRootViewGroup = DialogRootViewGroup(context) this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher + + // The dialog container's measured WIDTH is the authoritative canvas width: it + // already accounts for the window's horizontal insets, Material's max-width cap on + // tablets and the current rotation. DialogRootViewGroup's own updateNodeSize() path + // is a no-op on the new architecture (getNativeModule(UIManagerModule) returns null + // under Fabric), so this state channel is what actually gets the width across there. + // + // Its measured HEIGHT is deliberately ignored - see canvasHeight. + this.dialogRootViewGroup.setOnSizeChangeListener( + object : DialogRootViewGroup.OnSizeChangeListener { + override fun onSizeChange( + width: Int, + height: Int, + ) { + val density = context.resources.displayMetrics.density + pushCanvasSize(width / density, canvasHeight / density) + + // onSizeChanged fires from inside a layout pass, so defer the reposition: + // updateLayout() reads child heights that aren't final yet. This is what + // makes the sheet settle back into place after a rotation. It no-ops for + // fullHeight sheets, which is correct - those are pinned to the expanded + // offset either way. + if ((isOpen || isOpening) && !isClosing) { + post { + if ((isOpen || isOpening) && !isClosing) { + updateLayout() + } + } + } + } + }, + ) } SheetManager.add(this) } @@ -151,9 +199,84 @@ class BottomSheetView( r: Int, b: Int, ) { + this.seedCanvasSize() this.present() } + /** + * The height, in px, of the canvas the sheet content is laid out on. This is the whole + * expanded frame (the behavior's expandedOffset is the status bar height), NOT the + * sheet's current height. + * + * That distinction is the whole ballgame. The content's height is what drives the snap + * points, so the canvas has to be room to grow *into*. Sizing the canvas from the + * dialog's own measured height is circular - BottomSheetBehavior measures the dialog + * container against the sheet, so the canvas collapses onto the content height, and from + * then on the content is pinned: extra ScrollView padding (the Android keyboard path) or + * a longer list just becomes scroll extent instead of a height change, the + * OnLayoutChangeListener never fires, and the sheet stops responding to its content. + */ + private val canvasHeight: Float + get() = screenHeight - getStatusBarHeight() + + /** + * JS renders the sheet content unsized, so before the first state commit it measures + * 0x0 and present() has no content height to derive snap points from. Seed the canvas + * here to kick that off - the dialog container reports the authoritative width later, + * via its OnSizeChangeListener. + * + * Runs at most once per open cycle. It has to: each state commit re-fires onLayout, so + * re-seeding would fight the width the dialog reported and the two would push each other + * back and forth forever. + * + * stateWrapper is assigned while Fabric mounts the view, before the first layout pass, + * so setViewSize() should already reach the shadow tree from here. On the old + * architecture it is null and this no-ops, which is fine: DialogRootViewGroup's legacy + * updateNodeSize() path still sizes the content there. + */ + private fun seedCanvasSize() { + if (lastPushedCanvasWidth > 0f) return + val density = context.resources.displayMetrics.density + val widthPx = + minOf( + context.resources.displayMetrics.widthPixels + .toFloat(), + getMaxSheetWidth(), + ) + this.pushCanvasSize(widthPx / density, canvasHeight / density) + } + + /** + * Sets the size of this view's shadow node, which is the canvas the sheet content is + * laid out on. Deduped because both onLayout and the dialog container's size changes + * can re-report an unchanged size. + */ + private fun pushCanvasSize( + widthDp: Float, + heightDp: Float, + ) { + if (widthDp <= 0f || heightDp <= 0f) return + if (widthDp == lastPushedCanvasWidth && heightDp == lastPushedCanvasHeight) return + lastPushedCanvasWidth = widthDp + lastPushedCanvasHeight = heightDp + this.shadowNodeProxy.setViewSize(widthDp.toDouble(), heightDp.toDouble()) + } + + /** + * Material caps the sheet frame at `material_bottom_sheet_max_width` (the + * `android:maxWidth` on `Widget.MaterialComponents.BottomSheet`, which our dialog theme + * inherits from) and centers it horizontally, so on tablets the sheet is narrower than + * the display. Returns the cap in px. + */ + private fun getMaxSheetWidth(): Float = + try { + resources + .getDimensionPixelSize(com.google.android.material.R.dimen.material_bottom_sheet_max_width) + .toFloat() + } catch (e: android.content.res.Resources.NotFoundException) { + FALLBACK_MAX_SHEET_WIDTH_DP * context.resources.displayMetrics.density + } + private fun destroy() { this.stopObservingContentHeight() this.isClosing = false @@ -178,6 +301,15 @@ class BottomSheetView( val contentHeight = this.getContentHeight() + // The content is unsized until the canvas size we pushed lands in the shadow tree, + // so bail and let this retry itself: the state commit resizes this view, that + // re-fires onLayout, and onLayout re-enters present(). Full-height sheets don't + // need a content measurement, so they can go ahead immediately. + // + // Only gate when there is a state channel to wait on. Without one (old architecture) + // nothing would ever resize this view, and the sheet would never present. + if (stateWrapper != null && !fullHeight && contentHeight <= 0f) return + var activityWindow: Window? = null var currentContext = context while (currentContext != null) { @@ -425,8 +557,10 @@ class BottomSheetView( private fun getTargetHeight(): Float { val contentHeight = this.getContentHeight() + // maxHeight is stored unclamped, so clamp it against the current screen here + val effectiveMaxHeight = minOf(this.maxHeight, this.screenHeight) return when { - contentHeight > maxHeight -> maxHeight + contentHeight > effectiveMaxHeight -> effectiveMaxHeight contentHeight < minHeight -> minHeight else -> contentHeight } diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx index f2955395e6..f030633667 100644 --- a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx @@ -34,10 +34,6 @@ const IS_IOS15 = Platform.OS === 'ios' && // semvar - can be 3 segments, so can't use Number(Platform.Version) Number(Platform.Version.split('.').at(0)) < 16 -// older android versions (15 and below) aren't naturally edge-to-edge -// and behave a little differently -const IS_NON_E2E_ANDROID = - Platform.OS === 'android' && Number(Platform.Version) < 35 export class BottomSheetNativeComponent extends Component< BottomSheetViewProps, @@ -148,24 +144,35 @@ function BottomSheetNativeComponentInner({ const {height: screenHeight} = useWindowDimensions() const isHeightConstrained = maxHeight != null || rest.fullHeight === true - // sigh... on older Android versions, screenHeight does not include safe area insets - // on newer Androids + iOS, it does. we need to find the inner bit + the bottom inset - // for the sheet content - const sheetHeight = IS_NON_E2E_ANDROID - ? screenHeight + insets.bottom - : screenHeight - insets.top - return ( t.equals(mime, ignoreCase = true) } } - .filter { !it.name.contains("secure", ignoreCase = true) } - .filter { !(mime == MediaFormat.MIMETYPE_VIDEO_AVC && AVC_DENYLIST.contains(it.name)) } - - val hardware = candidates.filter { isHardware(it) } - val selected = if (requireHardware) { - hardware.firstOrNull() - } else { - hardware.firstOrNull() ?: candidates.firstOrNull() - } - selected ?: return null - return EncoderInfo( - name = selected.name, - mime = mime, - isHardware = isHardware(selected) - ) - } - - private fun isHardware(info: MediaCodecInfo): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - info.isHardwareAccelerated - } else { - SOFTWARE_PREFIXES.none { info.name.startsWith(it, ignoreCase = true) } - } - } -} diff --git a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/ExpoBlueskyVideoCompressModule.kt b/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/ExpoBlueskyVideoCompressModule.kt deleted file mode 100644 index 30b1def7c6..0000000000 --- a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/ExpoBlueskyVideoCompressModule.kt +++ /dev/null @@ -1,62 +0,0 @@ -package expo.modules.blueskyvideocompress - -import expo.modules.kotlin.modules.Module -import expo.modules.kotlin.modules.ModuleDefinition -import java.util.concurrent.ConcurrentHashMap - -class ExpoBlueskyVideoCompressModule : Module() { - private val activeCompressors = ConcurrentHashMap() - - override fun definition() = ModuleDefinition { - Name("ExpoBlueskyVideoCompress") - - Events("onProgress") - - AsyncFunction("probe") { uri: String -> - val context = appContext.reactContext - ?: throw Error("React context is null") - return@AsyncFunction VideoProber.probe(context, uri) - } - - AsyncFunction("compress") { uri: String, options: Map -> - val context = appContext.reactContext - ?: throw Error("React context is null") - val targetBitrate = (options["targetBitrate"] as? Number)?.toInt() ?: 0 - val maxSize = (options["maxSize"] as? Number)?.toInt() ?: 1920 - val codecPref = (options["codec"] as? String) ?: "auto" - val frameRateCap = ((options["frameRateCap"] as? Number)?.toInt() ?: 30).coerceAtLeast(1) - val jobId = (options["jobId"] as? Number)?.toInt() ?: 0 - - val compressor = VideoCompressor( - context = context, - uriString = uri, - targetBitrate = targetBitrate, - maxSize = maxSize, - codecPref = codecPref, - frameRateCap = frameRateCap, - jobId = jobId, - onProgress = { id, progress -> - sendEvent("onProgress", mapOf( - "id" to id, - "progress" to progress - )) - } - ) - - activeCompressors[jobId] = compressor - - try { - val result = compressor.compress() - activeCompressors.remove(jobId) - return@AsyncFunction result - } catch (e: Exception) { - activeCompressors.remove(jobId) - throw e - } - } - - Function("cancel") { jobId: Int -> - activeCompressors.remove(jobId)?.cancel() - } - } -} diff --git a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/InputSurface.kt b/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/InputSurface.kt deleted file mode 100644 index e2eee4768c..0000000000 --- a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/InputSurface.kt +++ /dev/null @@ -1,96 +0,0 @@ -package expo.modules.blueskyvideocompress - -import android.opengl.EGL14 -import android.opengl.EGLConfig -import android.opengl.EGLContext -import android.opengl.EGLDisplay -import android.opengl.EGLExt -import android.opengl.EGLSurface -import android.view.Surface - -class InputSurface(private val surface: Surface) { - private var eglDisplay: EGLDisplay = EGL14.EGL_NO_DISPLAY - private var eglContext: EGLContext = EGL14.EGL_NO_CONTEXT - private var eglSurface: EGLSurface = EGL14.EGL_NO_SURFACE - - init { - eglSetup() - } - - private fun eglSetup() { - eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY) - if (eglDisplay === EGL14.EGL_NO_DISPLAY) { - throw RuntimeException("unable to get EGL14 display") - } - - val version = IntArray(2) - if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { - throw RuntimeException("unable to initialize EGL14") - } - - val attribList = intArrayOf( - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_ALPHA_SIZE, 8, - EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT, - EGL14.EGL_NONE - ) - val configs = arrayOfNulls(1) - val numConfigs = IntArray(1) - EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, 1, numConfigs, 0) - checkEglError("eglChooseConfig") - - val contextAttribs = intArrayOf( - EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, - EGL14.EGL_NONE - ) - eglContext = EGL14.eglCreateContext( - eglDisplay, configs[0], EGL14.EGL_NO_CONTEXT, contextAttribs, 0 - ) - checkEglError("eglCreateContext") - - val surfaceAttribs = intArrayOf(EGL14.EGL_NONE) - eglSurface = EGL14.eglCreateWindowSurface( - eglDisplay, configs[0], surface, surfaceAttribs, 0 - ) - checkEglError("eglCreateWindowSurface") - } - - fun makeCurrent() { - EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) - checkEglError("eglMakeCurrent") - } - - fun swapBuffers(): Boolean { - return EGL14.eglSwapBuffers(eglDisplay, eglSurface) - } - - fun setPresentationTime(nsecs: Long) { - EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, nsecs) - } - - fun release() { - if (eglDisplay !== EGL14.EGL_NO_DISPLAY) { - EGL14.eglMakeCurrent( - eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT - ) - EGL14.eglDestroySurface(eglDisplay, eglSurface) - EGL14.eglDestroyContext(eglDisplay, eglContext) - EGL14.eglReleaseThread() - EGL14.eglTerminate(eglDisplay) - } - surface.release() - eglDisplay = EGL14.EGL_NO_DISPLAY - eglContext = EGL14.EGL_NO_CONTEXT - eglSurface = EGL14.EGL_NO_SURFACE - } - - private fun checkEglError(msg: String) { - val error = EGL14.eglGetError() - if (error != EGL14.EGL_SUCCESS) { - throw RuntimeException("$msg: EGL error: 0x${Integer.toHexString(error)}") - } - } -} diff --git a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/OutputSurface.kt b/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/OutputSurface.kt deleted file mode 100644 index fdf8c6eca2..0000000000 --- a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/OutputSurface.kt +++ /dev/null @@ -1,61 +0,0 @@ -package expo.modules.blueskyvideocompress - -import android.graphics.SurfaceTexture -import android.os.Handler -import android.os.HandlerThread -import android.view.Surface - -class OutputSurface : SurfaceTexture.OnFrameAvailableListener { - private val renderer = TextureRenderer() - private var surfaceTexture: SurfaceTexture? = null - private val stMatrix = FloatArray(16) - private val callbackThread = HandlerThread("OutputSurfaceCallbacks") - val surface: Surface - - @Volatile - private var frameAvailable = false - private val frameSyncObject = Object() - - init { - renderer.surfaceCreated() - callbackThread.start() - val handler = Handler(callbackThread.looper) - surfaceTexture = SurfaceTexture(renderer.getTextureId()).also { - it.setOnFrameAvailableListener(this, handler) - } - surface = Surface(surfaceTexture) - } - - fun release() { - surface.release() - surfaceTexture?.release() - surfaceTexture = null - callbackThread.quitSafely() - } - - fun awaitNewImage() { - val timeoutMs = 2500L - synchronized(frameSyncObject) { - while (!frameAvailable) { - frameSyncObject.wait(timeoutMs) - if (!frameAvailable) { - throw RuntimeException("Surface frame wait timed out") - } - } - frameAvailable = false - } - surfaceTexture!!.updateTexImage() - } - - fun drawImage() { - surfaceTexture!!.getTransformMatrix(stMatrix) - renderer.drawFrame(stMatrix) - } - - override fun onFrameAvailable(st: SurfaceTexture) { - synchronized(frameSyncObject) { - frameAvailable = true - frameSyncObject.notifyAll() - } - } -} diff --git a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/TextureRenderer.kt b/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/TextureRenderer.kt deleted file mode 100644 index a0cb25bce1..0000000000 --- a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/TextureRenderer.kt +++ /dev/null @@ -1,156 +0,0 @@ -package expo.modules.blueskyvideocompress - -import android.opengl.GLES11Ext -import android.opengl.GLES20 -import android.opengl.Matrix -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.nio.FloatBuffer - -class TextureRenderer { - companion object { - private const val FLOAT_SIZE_BYTES = 4 - private const val STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES - private const val POS_OFFSET = 0 - private const val UV_OFFSET = 3 - - private val VERTICES = floatArrayOf( - -1.0f, -1.0f, 0f, 0f, 0f, - 1.0f, -1.0f, 0f, 1f, 0f, - -1.0f, 1.0f, 0f, 0f, 1f, - 1.0f, 1.0f, 0f, 1f, 1f, - ) - - private const val VERTEX_SHADER = """ - uniform mat4 uMVPMatrix; - uniform mat4 uSTMatrix; - attribute vec4 aPosition; - attribute vec4 aTextureCoord; - varying vec2 vTextureCoord; - void main() { - gl_Position = uMVPMatrix * aPosition; - vTextureCoord = (uSTMatrix * aTextureCoord).xy; - } - """ - - private const val FRAGMENT_SHADER = """ - #extension GL_OES_EGL_image_external : require - precision mediump float; - varying vec2 vTextureCoord; - uniform samplerExternalOES sTexture; - void main() { - gl_FragColor = texture2D(sTexture, vTextureCoord); - } - """ - } - - private val vertices: FloatBuffer = - ByteBuffer.allocateDirect(VERTICES.size * FLOAT_SIZE_BYTES) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer() - .apply { put(VERTICES); position(0) } - - private val mvpMatrix = FloatArray(16) - private var program = 0 - private var textureId = -1 - private var uMVPMatrixHandle = 0 - private var uSTMatrixHandle = 0 - private var aPositionHandle = 0 - private var aTextureCoordHandle = 0 - - init { - Matrix.setIdentityM(mvpMatrix, 0) - } - - fun getTextureId(): Int = textureId - - fun surfaceCreated() { - program = createProgram(VERTEX_SHADER, FRAGMENT_SHADER) - - aPositionHandle = GLES20.glGetAttribLocation(program, "aPosition") - aTextureCoordHandle = GLES20.glGetAttribLocation(program, "aTextureCoord") - uMVPMatrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix") - uSTMatrixHandle = GLES20.glGetUniformLocation(program, "uSTMatrix") - - val textures = IntArray(1) - GLES20.glGenTextures(1, textures, 0) - textureId = textures[0] - - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId) - GLES20.glTexParameterf( - GLES11Ext.GL_TEXTURE_EXTERNAL_OES, - GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat() - ) - GLES20.glTexParameterf( - GLES11Ext.GL_TEXTURE_EXTERNAL_OES, - GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat() - ) - GLES20.glTexParameteri( - GLES11Ext.GL_TEXTURE_EXTERNAL_OES, - GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE - ) - GLES20.glTexParameteri( - GLES11Ext.GL_TEXTURE_EXTERNAL_OES, - GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE - ) - } - - fun drawFrame(stMatrix: FloatArray) { - GLES20.glClearColor(0f, 0f, 0f, 1f) - GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT or GLES20.GL_COLOR_BUFFER_BIT) - - GLES20.glUseProgram(program) - GLES20.glActiveTexture(GLES20.GL_TEXTURE0) - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId) - - vertices.position(POS_OFFSET) - GLES20.glVertexAttribPointer( - aPositionHandle, 3, GLES20.GL_FLOAT, false, STRIDE_BYTES, vertices - ) - GLES20.glEnableVertexAttribArray(aPositionHandle) - - vertices.position(UV_OFFSET) - GLES20.glVertexAttribPointer( - aTextureCoordHandle, 2, GLES20.GL_FLOAT, false, STRIDE_BYTES, vertices - ) - GLES20.glEnableVertexAttribArray(aTextureCoordHandle) - - GLES20.glUniformMatrix4fv(uMVPMatrixHandle, 1, false, mvpMatrix, 0) - GLES20.glUniformMatrix4fv(uSTMatrixHandle, 1, false, stMatrix, 0) - - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) - GLES20.glDisableVertexAttribArray(aPositionHandle) - GLES20.glDisableVertexAttribArray(aTextureCoordHandle) - } - - private fun createProgram(vertexSource: String, fragmentSource: String): Int { - val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource) - val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource) - val program = GLES20.glCreateProgram() - GLES20.glAttachShader(program, vertexShader) - GLES20.glAttachShader(program, fragmentShader) - GLES20.glLinkProgram(program) - val linkStatus = IntArray(1) - GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0) - if (linkStatus[0] != GLES20.GL_TRUE) { - val log = GLES20.glGetProgramInfoLog(program) - GLES20.glDeleteProgram(program) - throw RuntimeException("Could not link program: $log") - } - return program - } - - private fun loadShader(type: Int, source: String): Int { - val shader = GLES20.glCreateShader(type) - GLES20.glShaderSource(shader, source) - GLES20.glCompileShader(shader) - val compiled = IntArray(1) - GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0) - if (compiled[0] == 0) { - val log = GLES20.glGetShaderInfoLog(shader) - GLES20.glDeleteShader(shader) - throw RuntimeException("Could not compile shader $type: $log") - } - return shader - } -} diff --git a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/VideoCompressor.kt b/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/VideoCompressor.kt deleted file mode 100644 index e9c99bc9c4..0000000000 --- a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/VideoCompressor.kt +++ /dev/null @@ -1,582 +0,0 @@ -package expo.modules.blueskyvideocompress - -import android.content.Context -import android.media.MediaCodec -import android.media.MediaCodecInfo -import android.media.MediaExtractor -import android.media.MediaFormat -import android.media.MediaMuxer -import android.net.Uri -import android.os.Build -import android.util.Log -import java.io.File -import java.nio.ByteBuffer -import java.util.UUID - -class VideoCompressor( - private val context: Context, - private val uriString: String, - private val targetBitrate: Int, - private val maxSize: Int, - private val codecPref: String, - private val frameRateCap: Int, - private val jobId: Int, - private val onProgress: (Int, Double) -> Unit -) { - companion object { - private const val TAG = "BskyVideoCompress" - private const val TIMEOUT_DEQUEUE = 100L - private const val I_FRAME_INTERVAL = 3 - } - - @Volatile - private var isCancelled = false - - fun cancel() { - isCancelled = true - } - - fun compress(): Map { - // 'auto' targets h264 — server pipeline is HLS, which favors h264 (HEVC needs - // fMP4 segments + commercial licensing). HEVC remains opt-in via codec: 'hevc'. - val tryHevc = codecPref == "hevc" - - if (tryHevc) { - try { - return doCompress(useHevc = true, allowSoftwareFallback = false) - } catch (e: Exception) { - if (codecPref == "hevc" || isCancelled) throw e - Log.w(TAG, "HEVC encode failed, falling back to h264", e) - } - } - return doCompress(useHevc = false, allowSoftwareFallback = true) - } - - private fun doCompress(useHevc: Boolean, allowSoftwareFallback: Boolean): Map { - val mime = if (useHevc) MediaFormat.MIMETYPE_VIDEO_HEVC else MediaFormat.MIMETYPE_VIDEO_AVC - val encoderInfo = CodecSelector.findEncoder(mime, requireHardware = !allowSoftwareFallback) - ?: throw RuntimeException("No encoder for $mime") - - try { - return runPipeline(encoderInfo, useHevc) - } catch (e: Exception) { - if (!allowSoftwareFallback || isCancelled || !encoderInfo.isHardware) throw e - Log.w(TAG, "Hardware encoder ${encoderInfo.name} failed, trying software", e) - val sw = CodecSelector.findEncoder(mime, requireHardware = false) - ?.takeIf { !it.isHardware } - ?: throw e - return runPipeline(sw, useHevc) - } - } - - private fun runPipeline( - encoderInfo: CodecSelector.EncoderInfo, - useHevc: Boolean - ): Map { - val outputFile = File(context.cacheDir, "${UUID.randomUUID()}.mp4") - - var extractor: MediaExtractor? = null - var muxer: MediaMuxer? = null - var encoder: MediaCodec? = null - var decoder: MediaCodec? = null - var inputSurface: InputSurface? = null - var outputSurface: OutputSurface? = null - var muxerStarted = false - var outputDims = Pair(0, 0) - var durationUs = 0L - - try { - val uri = Uri.parse(uriString) - extractor = MediaExtractor() - if (uriString.startsWith("content://") || uriString.startsWith("file://")) { - extractor.setDataSource(context, uri, null) - } else { - extractor.setDataSource(uriString) - } - - var videoTrackIndex = -1 - var audioTrackIndex = -1 - var videoFormat: MediaFormat? = null - var audioFormat: MediaFormat? = null - - for (i in 0 until extractor.trackCount) { - val format = extractor.getTrackFormat(i) - val trackMime = format.getString(MediaFormat.KEY_MIME) ?: continue - if (trackMime.startsWith("video/") && videoTrackIndex == -1) { - videoTrackIndex = i - videoFormat = format - } else if (trackMime.startsWith("audio/") && audioTrackIndex == -1) { - audioTrackIndex = i - audioFormat = format - } - } - - if (videoTrackIndex == -1 || videoFormat == null) { - throw RuntimeException("No video track found") - } - - val sourceWidth = videoFormat.getInteger(MediaFormat.KEY_WIDTH) - val sourceHeight = videoFormat.getInteger(MediaFormat.KEY_HEIGHT) - val rotation = if (videoFormat.containsKey(MediaFormat.KEY_ROTATION)) { - videoFormat.getInteger(MediaFormat.KEY_ROTATION) - } else 0 - durationUs = if (videoFormat.containsKey(MediaFormat.KEY_DURATION)) { - videoFormat.getLong(MediaFormat.KEY_DURATION) - } else 0L - val sourceFps = if (videoFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) { - videoFormat.getInteger(MediaFormat.KEY_FRAME_RATE) - } else 30 - - outputDims = calculateOutputDims(sourceWidth, sourceHeight, rotation, maxSize) - val shouldPassthroughAudio = audioFormat != null && canPassthroughAudio(audioFormat) - val transcodedAudio: TranscodedAudio? = if ( - audioTrackIndex >= 0 && audioFormat != null && !shouldPassthroughAudio - ) { - transcodeAudioToAAC(audioTrackIndex, audioFormat) - } else null - muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) - - val effectiveBitrate = if (targetBitrate > 0) { - targetBitrate - } else if (useHevc) 2_500_000 else 3_000_000 - - val encoderFormat = MediaFormat.createVideoFormat( - encoderInfo.mime, outputDims.first, outputDims.second - ).apply { - setInteger( - MediaFormat.KEY_COLOR_FORMAT, - MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface - ) - setInteger(MediaFormat.KEY_BIT_RATE, effectiveBitrate) - setInteger( - MediaFormat.KEY_BITRATE_MODE, - MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR - ) - setInteger(MediaFormat.KEY_FRAME_RATE, frameRateCap) - setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT709) - setInteger(MediaFormat.KEY_COLOR_TRANSFER, MediaFormat.COLOR_TRANSFER_SDR_VIDEO) - setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setInteger(MediaFormat.KEY_PRIORITY, 0) - setInteger(MediaFormat.KEY_OPERATING_RATE, frameRateCap) - if (useHevc) { - setInteger( - MediaFormat.KEY_PROFILE, - MediaCodecInfo.CodecProfileLevel.HEVCProfileMain - ) - } else { - setInteger( - MediaFormat.KEY_PROFILE, - MediaCodecInfo.CodecProfileLevel.AVCProfileHigh - ) - setInteger( - MediaFormat.KEY_LEVEL, - MediaCodecInfo.CodecProfileLevel.AVCLevel41 - ) - } - } - } - - encoder = MediaCodec.createByCodecName(encoderInfo.name) - encoder.configure(encoderFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) - val encoderInputSurface = encoder.createInputSurface() - inputSurface = InputSurface(encoderInputSurface) - inputSurface.makeCurrent() - outputSurface = OutputSurface() - encoder.start() - - decoder = MediaCodec.createDecoderByType( - videoFormat.getString(MediaFormat.KEY_MIME) ?: "video/avc" - ) - // Ask the decoder to tone-map HDR (HLG/PQ) sources to SDR. Vendors may - // ignore the hint, but where supported it produces correct BT.709 pixels - // for the encoder rather than HDR pixels mislabeled as SDR. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - videoFormat.setInteger( - MediaFormat.KEY_COLOR_TRANSFER_REQUEST, - MediaFormat.COLOR_TRANSFER_SDR_VIDEO - ) - } - decoder.configure(videoFormat, outputSurface.surface, null, 0) - decoder.start() - extractor.selectTrack(videoTrackIndex) - - val frameDropEnabled = sourceFps > frameRateCap - val targetFrameIntervalUs = if (frameDropEnabled) 1_000_000L / frameRateCap else 0L - var nextTargetPtsUs = 0L - - var muxerVideoTrack = -1 - var muxerAudioTrack = -1 - - val bufferInfo = MediaCodec.BufferInfo() - var inputDone = false - var decoderDone = false - var outputDone = false - var lastProgressMs = 0L - - while (!outputDone && !isCancelled) { - if (!inputDone) { - val idx = decoder.dequeueInputBuffer(TIMEOUT_DEQUEUE) - if (idx >= 0) { - val buf = decoder.getInputBuffer(idx) - if (buf != null) { - val sz = extractor.readSampleData(buf, 0) - if (sz < 0) { - decoder.queueInputBuffer( - idx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM - ) - inputDone = true - } else { - decoder.queueInputBuffer(idx, 0, sz, extractor.sampleTime, 0) - extractor.advance() - } - } - } - } - - if (!decoderDone) { - val status = decoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_DEQUEUE) - if (status >= 0) { - val isEos = bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0 - val shouldRender = if (isEos) { - false - } else if (frameDropEnabled) { - if (bufferInfo.presentationTimeUs >= nextTargetPtsUs) { - nextTargetPtsUs = bufferInfo.presentationTimeUs + targetFrameIntervalUs - true - } else false - } else true - decoder.releaseOutputBuffer(status, shouldRender) - if (shouldRender) { - outputSurface.awaitNewImage() - outputSurface.drawImage() - inputSurface.setPresentationTime(bufferInfo.presentationTimeUs * 1000) - inputSurface.swapBuffers() - } - if (isEos) { - encoder.signalEndOfInputStream() - decoderDone = true - } - } - } - - var encoderDrained = false - while (!outputDone && !isCancelled && !encoderDrained) { - val encIdx = encoder.dequeueOutputBuffer(bufferInfo, 0) - when { - encIdx == MediaCodec.INFO_TRY_AGAIN_LATER -> encoderDrained = true - encIdx == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { - if (!muxerStarted) { - muxerVideoTrack = muxer.addTrack(encoder.outputFormat) - if (audioTrackIndex >= 0 && audioFormat != null) { - if (shouldPassthroughAudio) { - muxerAudioTrack = muxer.addTrack(audioFormat) - } else if (transcodedAudio != null) { - muxerAudioTrack = muxer.addTrack(transcodedAudio.outputFormat) - } - } - muxer.start() - muxerStarted = true - } - } - encIdx >= 0 -> { - val data = encoder.getOutputBuffer(encIdx) - if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) { - bufferInfo.size = 0 - } - if (data != null && bufferInfo.size > 0 && muxerStarted) { - muxer.writeSampleData(muxerVideoTrack, data, bufferInfo) - } - val isEos = bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0 - encoder.releaseOutputBuffer(encIdx, false) - if (isEos) { - outputDone = true - } else if (durationUs > 0) { - val now = System.currentTimeMillis() - if (now - lastProgressMs >= 100) { - lastProgressMs = now - val p = (bufferInfo.presentationTimeUs.toDouble() / durationUs) - .coerceIn(0.0, 1.0) - onProgress(jobId, p) - } - } - } - } - } - } - - if (audioTrackIndex >= 0 && muxerAudioTrack >= 0 && muxerStarted && !isCancelled) { - if (shouldPassthroughAudio) { - passthroughAudio(audioTrackIndex, muxer, muxerAudioTrack) - } else if (transcodedAudio != null) { - writeTranscodedAudio(transcodedAudio.samples, muxer, muxerAudioTrack) - } - } - } finally { - try { decoder?.stop() } catch (_: Exception) {} - try { decoder?.release() } catch (_: Exception) {} - try { encoder?.stop() } catch (_: Exception) {} - try { encoder?.release() } catch (_: Exception) {} - try { outputSurface?.release() } catch (_: Exception) {} - try { inputSurface?.release() } catch (_: Exception) {} - try { extractor?.release() } catch (_: Exception) {} - try { - if (muxerStarted) muxer?.stop() - muxer?.release() - } catch (_: Exception) {} - } - - if (isCancelled) { - outputFile.delete() - throw RuntimeException("Compression cancelled") - } - - onProgress(jobId, 1.0) - val durationSeconds = durationUs / 1_000_000.0 - - return mapOf( - "uri" to "file://${outputFile.absolutePath}", - "size" to outputFile.length(), - "mimeType" to "video/mp4", - "width" to outputDims.first, - "height" to outputDims.second, - "duration" to durationSeconds, - "codec" to if (useHevc) "hevc" else "h264" - ) - } - - private fun passthroughAudio(audioTrackIndex: Int, muxer: MediaMuxer, muxerAudioTrack: Int) { - val audioExtractor = MediaExtractor() - if (uriString.startsWith("content://") || uriString.startsWith("file://")) { - audioExtractor.setDataSource(context, Uri.parse(uriString), null) - } else { - audioExtractor.setDataSource(uriString) - } - audioExtractor.selectTrack(audioTrackIndex) - audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC) - - val buffer = ByteBuffer.allocate(256 * 1024) - val info = MediaCodec.BufferInfo() - try { - while (!isCancelled) { - val sz = audioExtractor.readSampleData(buffer, 0) - if (sz < 0) break - info.offset = 0 - info.size = sz - info.presentationTimeUs = audioExtractor.sampleTime - info.flags = audioExtractor.sampleFlags - muxer.writeSampleData(muxerAudioTrack, buffer, info) - audioExtractor.advance() - } - } finally { - audioExtractor.release() - } - } - - private fun canPassthroughAudio(format: MediaFormat): Boolean { - val mime = format.getString(MediaFormat.KEY_MIME) ?: return false - return mime == MediaFormat.MIMETYPE_AUDIO_AAC - } - - private data class TranscodedAudio( - val outputFormat: MediaFormat, - val samples: List - ) { - data class Sample( - val bytes: ByteArray, - val presentationTimeUs: Long, - val flags: Int - ) - } - - // Re-encode non-AAC source audio (Opus, Vorbis, etc.) to AAC so the mp4 muxer - // can take it. iOS always re-encodes to AAC; without this Android would drop - // the audio track entirely. - private fun transcodeAudioToAAC( - audioTrackIndex: Int, - sourceFormat: MediaFormat - ): TranscodedAudio? { - val sourceMime = sourceFormat.getString(MediaFormat.KEY_MIME) ?: return null - val sampleRate = if (sourceFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) - sourceFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE) else 44100 - val channelCount = if (sourceFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) - sourceFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT).coerceIn(1, 2) else 2 - - val audioExtractor = MediaExtractor() - if (uriString.startsWith("content://") || uriString.startsWith("file://")) { - audioExtractor.setDataSource(context, Uri.parse(uriString), null) - } else { - audioExtractor.setDataSource(uriString) - } - audioExtractor.selectTrack(audioTrackIndex) - - var decoder: MediaCodec? = null - var encoder: MediaCodec? = null - try { - decoder = MediaCodec.createDecoderByType(sourceMime) - decoder.configure(sourceFormat, null, null, 0) - decoder.start() - - val encoderFormat = MediaFormat.createAudioFormat( - MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, channelCount - ).apply { - setInteger( - MediaFormat.KEY_AAC_PROFILE, - MediaCodecInfo.CodecProfileLevel.AACObjectLC - ) - setInteger(MediaFormat.KEY_BIT_RATE, 128_000) - setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 256 * 1024) - } - encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC) - encoder.configure(encoderFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) - encoder.start() - - val samples = mutableListOf() - var outputFormat: MediaFormat? = null - var inputDone = false - var decoderDone = false - var encoderInputSignalled = false - var encoderDone = false - val info = MediaCodec.BufferInfo() - - while (!encoderDone && !isCancelled) { - if (!inputDone) { - val idx = decoder.dequeueInputBuffer(TIMEOUT_DEQUEUE) - if (idx >= 0) { - val buf = decoder.getInputBuffer(idx) - if (buf != null) { - val sz = audioExtractor.readSampleData(buf, 0) - if (sz < 0) { - decoder.queueInputBuffer( - idx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM - ) - inputDone = true - } else { - decoder.queueInputBuffer( - idx, 0, sz, audioExtractor.sampleTime, audioExtractor.sampleFlags - ) - audioExtractor.advance() - } - } - } - } - - if (!decoderDone) { - val status = decoder.dequeueOutputBuffer(info, TIMEOUT_DEQUEUE) - when { - status == MediaCodec.INFO_TRY_AGAIN_LATER -> {} - status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {} - status >= 0 -> { - val isEos = info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0 - val data = decoder.getOutputBuffer(status) - if (data != null && info.size > 0) { - val encInIdx = encoder.dequeueInputBuffer(TIMEOUT_DEQUEUE) - if (encInIdx >= 0) { - val encInBuf = encoder.getInputBuffer(encInIdx) - if (encInBuf != null) { - encInBuf.clear() - data.position(info.offset) - data.limit(info.offset + info.size) - encInBuf.put(data) - encoder.queueInputBuffer( - encInIdx, 0, info.size, info.presentationTimeUs, 0 - ) - } - } - } - decoder.releaseOutputBuffer(status, false) - if (isEos) { - if (!encoderInputSignalled) { - val encInIdx = encoder.dequeueInputBuffer(TIMEOUT_DEQUEUE * 10) - if (encInIdx >= 0) { - encoder.queueInputBuffer( - encInIdx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM - ) - encoderInputSignalled = true - } - } - decoderDone = true - } - } - } - } - - val encOutIdx = encoder.dequeueOutputBuffer(info, TIMEOUT_DEQUEUE) - when { - encOutIdx == MediaCodec.INFO_TRY_AGAIN_LATER -> {} - encOutIdx == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { - outputFormat = encoder.outputFormat - } - encOutIdx >= 0 -> { - val data = encoder.getOutputBuffer(encOutIdx) - val isEos = info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0 - val isConfig = info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0 - if (data != null && info.size > 0 && !isConfig) { - val bytes = ByteArray(info.size) - data.position(info.offset) - data.get(bytes, 0, info.size) - samples.add( - TranscodedAudio.Sample( - bytes = bytes, - presentationTimeUs = info.presentationTimeUs, - flags = info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG.inv() - ) - ) - } - encoder.releaseOutputBuffer(encOutIdx, false) - if (isEos) encoderDone = true - } - } - } - - val fmt = outputFormat ?: return null - return TranscodedAudio(fmt, samples) - } catch (e: Exception) { - Log.w(TAG, "Audio transcode failed; dropping audio", e) - return null - } finally { - try { decoder?.stop() } catch (_: Exception) {} - try { decoder?.release() } catch (_: Exception) {} - try { encoder?.stop() } catch (_: Exception) {} - try { encoder?.release() } catch (_: Exception) {} - audioExtractor.release() - } - } - - private fun writeTranscodedAudio( - samples: List, - muxer: MediaMuxer, - muxerAudioTrack: Int - ) { - val info = MediaCodec.BufferInfo() - for (sample in samples) { - if (isCancelled) break - val buffer = ByteBuffer.wrap(sample.bytes) - info.offset = 0 - info.size = sample.bytes.size - info.presentationTimeUs = sample.presentationTimeUs - info.flags = sample.flags - muxer.writeSampleData(muxerAudioTrack, buffer, info) - } - } - - private fun calculateOutputDims(srcW: Int, srcH: Int, rotation: Int, maxSize: Int): Pair { - val isRotated = rotation == 90 || rotation == 270 - val displayW = if (isRotated) srcH else srcW - val displayH = if (isRotated) srcW else srcH - val scale: Float = when { - displayW <= maxSize && displayH <= maxSize -> 1.0f - displayW > displayH -> maxSize.toFloat() / displayW - else -> maxSize.toFloat() / displayH - } - return Pair( - roundToEven((displayW * scale).toInt()), - roundToEven((displayH * scale).toInt()) - ) - } - - private fun roundToEven(v: Int): Int = if (v % 2 == 0) v else v - 1 -} diff --git a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/VideoProber.kt b/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/VideoProber.kt deleted file mode 100644 index 7c214bddd0..0000000000 --- a/modules/expo-bluesky-video-compress/android/src/main/java/expo/modules/blueskyvideocompress/VideoProber.kt +++ /dev/null @@ -1,125 +0,0 @@ -package expo.modules.blueskyvideocompress - -import android.content.Context -import android.media.MediaExtractor -import android.media.MediaFormat -import android.media.MediaMetadataRetriever -import android.net.Uri -import android.os.Build - -object VideoProber { - fun probe(context: Context, uriString: String): Map { - val uri = Uri.parse(uriString) - val retriever = MediaMetadataRetriever() - - try { - if (uriString.startsWith("content://") || uriString.startsWith("file://")) { - retriever.setDataSource(context, uri) - } else { - retriever.setDataSource(uriString) - } - - val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) - ?.toIntOrNull() ?: 0 - val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) - ?.toIntOrNull() ?: 0 - val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) - ?.toLongOrNull() ?: 0L - val rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) - ?.toIntOrNull() ?: 0 - val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE) - ?.toIntOrNull() ?: 0 - val hasAudio = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) - ?.equals("yes") ?: false - val frameRate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE) - ?.toFloatOrNull() ?: 0f - - val fileSize = getFileSize(context, uriString) - - val extractor = MediaExtractor() - var codec = "unknown" - var mimeType = "video/mp4" - var extractedFrameRate = frameRate - var isHDR = false - - try { - if (uriString.startsWith("content://") || uriString.startsWith("file://")) { - extractor.setDataSource(context, uri, null) - } else { - extractor.setDataSource(uriString) - } - - for (i in 0 until extractor.trackCount) { - val format = extractor.getTrackFormat(i) - val mime = format.getString(MediaFormat.KEY_MIME) - if (mime?.startsWith("video/") == true) { - mimeType = mime - codec = mime.removePrefix("video/") - if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) { - extractedFrameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE).toFloat() - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && - format.containsKey(MediaFormat.KEY_COLOR_TRANSFER) - ) { - val transfer = format.getInteger(MediaFormat.KEY_COLOR_TRANSFER) - isHDR = transfer == MediaFormat.COLOR_TRANSFER_HLG || - transfer == MediaFormat.COLOR_TRANSFER_ST2084 - } - // Dolby Vision tracks use codec-specific mimes that aren't covered by - // KEY_COLOR_TRANSFER on every device. - if (mime.contains("dolby-vision", ignoreCase = true)) { - isHDR = true - } - break - } - } - } finally { - extractor.release() - } - - val durationSeconds = durationMs / 1000.0 - val effectiveBitrate = if (bitrate > 0) { - bitrate - } else if (durationSeconds > 0 && fileSize > 0) { - (fileSize * 8 / durationSeconds).toInt() - } else { - 0 - } - - return mapOf( - "width" to width, - "height" to height, - "duration" to durationSeconds, - "bitrate" to effectiveBitrate, - "fileSize" to fileSize, - "mimeType" to mimeType, - "codec" to codec, - "hasAudio" to hasAudio, - "frameRate" to extractedFrameRate.toDouble(), - "rotation" to rotation, - "isHDR" to isHDR - ) - } finally { - retriever.release() - } - } - - private fun getFileSize(context: Context, uriString: String): Long { - return try { - if (uriString.startsWith("content://")) { - context.contentResolver.openFileDescriptor(Uri.parse(uriString), "r")?.use { - it.statSize - } ?: 0L - } else { - val path = if (uriString.startsWith("file://")) { - uriString.removePrefix("file://") - } else { - uriString - } - java.io.File(path).length() - } - } catch (_: Exception) { - 0L - } - } -} diff --git a/modules/expo-bluesky-video-compress/expo-module.config.json b/modules/expo-bluesky-video-compress/expo-module.config.json deleted file mode 100644 index c1c24a480d..0000000000 --- a/modules/expo-bluesky-video-compress/expo-module.config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "platforms": ["ios", "android"], - "ios": { - "modules": ["ExpoBlueskyVideoCompressModule"] - }, - "android": { - "modules": ["expo.modules.blueskyvideocompress.ExpoBlueskyVideoCompressModule"] - } -} diff --git a/modules/expo-bluesky-video-compress/index.ts b/modules/expo-bluesky-video-compress/index.ts deleted file mode 100644 index 423d7f3ac2..0000000000 --- a/modules/expo-bluesky-video-compress/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -import {type EventSubscription} from 'expo-modules-core' - -import NativeModule from './src/ExpoBlueskyVideoCompressModule' -import { - type CodecPreference, - type CompressCallbacks, - type CompressOptions, - type CompressResult, - type VideoMetadata, -} from './src/types' - -export type { - CodecPreference, - CompressCallbacks, - CompressOptions, - CompressResult, - VideoMetadata, -} - -class AbortError extends Error { - name = 'AbortError' - constructor() { - super('Aborted') - } -} - -let jobIdCounter = 0 - -export function probe(uri: string): Promise { - return NativeModule.probe(uri) -} - -export function compress( - uri: string, - options: CompressOptions = {}, - callbacks?: CompressCallbacks, -): Promise { - const jobId = ++jobIdCounter - let subscription: EventSubscription | undefined - - if (callbacks?.signal?.aborted) { - return Promise.reject(new AbortError()) - } - - const nativeOptions = { - targetBitrate: options.targetBitrate ?? 0, - maxSize: options.maxSize ?? 1920, - codec: options.codec ?? 'auto', - frameRateCap: options.frameRateCap ?? 30, - jobId, - } - - return new Promise((resolve, reject) => { - if (callbacks?.onProgress) { - subscription = NativeModule.addListener( - 'onProgress', - (event: {id: number; progress: number}) => { - if (event.id === jobId) { - callbacks.onProgress!(event.progress) - } - }, - ) - } - - const abortHandler = () => { - NativeModule.cancel(jobId) - subscription?.remove() - reject(new AbortError()) - } - - if (callbacks?.signal) { - callbacks.signal.addEventListener('abort', abortHandler, {once: true}) - } - - NativeModule.compress(uri, nativeOptions) - .then(result => { - callbacks?.signal?.removeEventListener('abort', abortHandler) - subscription?.remove() - resolve(result) - }) - .catch((error: unknown) => { - callbacks?.signal?.removeEventListener('abort', abortHandler) - subscription?.remove() - reject(error instanceof Error ? error : new Error(String(error))) - }) - }) -} diff --git a/modules/expo-bluesky-video-compress/ios/CodecCapability.swift b/modules/expo-bluesky-video-compress/ios/CodecCapability.swift deleted file mode 100644 index df95d9b7e8..0000000000 --- a/modules/expo-bluesky-video-compress/ios/CodecCapability.swift +++ /dev/null @@ -1,21 +0,0 @@ -import VideoToolbox - -enum CodecCapability { - static let isHardwareHEVCEncodeAvailable: Bool = { - var encoderListCF: CFArray? - let status = VTCopyVideoEncoderList(nil, &encoderListCF) - guard status == noErr, let encoderList = encoderListCF as? [[String: Any]] else { - return false - } - return encoderList.contains { encoder in - guard let codecTypeValue = encoder[kVTVideoEncoderList_CodecType as String] as? Int, - codecTypeValue == Int(kCMVideoCodecType_HEVC) else { - return false - } - if let isHardware = encoder[kVTVideoEncoderList_IsHardwareAccelerated as String] as? Bool { - return isHardware - } - return true - } - }() -} diff --git a/modules/expo-bluesky-video-compress/ios/ExpoBlueskyVideoCompress.podspec b/modules/expo-bluesky-video-compress/ios/ExpoBlueskyVideoCompress.podspec deleted file mode 100644 index 5bca8f0812..0000000000 --- a/modules/expo-bluesky-video-compress/ios/ExpoBlueskyVideoCompress.podspec +++ /dev/null @@ -1,20 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'ExpoBlueskyVideoCompress' - s.version = '1.0.0' - s.summary = 'Hardware-accelerated video compression for Bluesky' - s.description = 'Hardware-accelerated h264/HEVC video compression using AVAssetReader/Writer and VideoToolbox on iOS' - s.author = '' - s.homepage = 'https://github.com/bluesky-social/social-app' - s.platforms = { :ios => '15.1' } - s.source = { git: '' } - s.static_framework = true - - s.dependency 'ExpoModulesCore' - - s.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - 'SWIFT_COMPILATION_MODE' => 'wholemodule' - } - - s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" -end diff --git a/modules/expo-bluesky-video-compress/ios/ExpoBlueskyVideoCompressModule.swift b/modules/expo-bluesky-video-compress/ios/ExpoBlueskyVideoCompressModule.swift deleted file mode 100644 index 3bcea3c9f4..0000000000 --- a/modules/expo-bluesky-video-compress/ios/ExpoBlueskyVideoCompressModule.swift +++ /dev/null @@ -1,74 +0,0 @@ -import AVFoundation -import ExpoModulesCore - -public class ExpoBlueskyVideoCompressModule: Module { - private var activeCompressors: [Int: VideoCompressor] = [:] - private let activeCompressorsLock = NSLock() - - public func definition() -> ModuleDefinition { - Name("ExpoBlueskyVideoCompress") - - Events("onProgress") - - AsyncFunction("probe") { (uri: String) -> [String: Any] in - let url = URL(string: uri) ?? URL(fileURLWithPath: uri) - return try await VideoProber.probe(url: url) - } - - AsyncFunction("compress") { (uri: String, options: [String: Any]) -> [String: Any] in - let url = URL(string: uri) ?? URL(fileURLWithPath: uri) - let targetBitrate = options["targetBitrate"] as? Int ?? 0 - let maxSize = options["maxSize"] as? Int ?? 1920 - let codecPref = options["codec"] as? String ?? "auto" - let frameRateCap = max(1, options["frameRateCap"] as? Int ?? 30) - let jobId = options["jobId"] as? Int ?? 0 - - let compressor = VideoCompressor( - url: url, - targetBitrate: targetBitrate, - maxSize: maxSize, - codecPref: codecPref, - frameRateCap: frameRateCap, - jobId: jobId, - onProgress: { [weak self] id, progress in - self?.sendEvent("onProgress", [ - "id": id, - "progress": progress, - ]) - } - ) - - self.setCompressor(jobId, compressor) - - do { - let result = try await compressor.compress() - self.setCompressor(jobId, nil) - return result - } catch { - self.setCompressor(jobId, nil) - throw error - } - } - - Function("cancel") { (jobId: Int) in - self.cancelCompressor(jobId) - } - } - - private func setCompressor(_ jobId: Int, _ compressor: VideoCompressor?) { - activeCompressorsLock.lock() - defer { activeCompressorsLock.unlock() } - if let compressor = compressor { - activeCompressors[jobId] = compressor - } else { - activeCompressors.removeValue(forKey: jobId) - } - } - - private func cancelCompressor(_ jobId: Int) { - activeCompressorsLock.lock() - let compressor = activeCompressors.removeValue(forKey: jobId) - activeCompressorsLock.unlock() - compressor?.cancel() - } -} diff --git a/modules/expo-bluesky-video-compress/ios/VideoCompressor.swift b/modules/expo-bluesky-video-compress/ios/VideoCompressor.swift deleted file mode 100644 index e4a75398ea..0000000000 --- a/modules/expo-bluesky-video-compress/ios/VideoCompressor.swift +++ /dev/null @@ -1,410 +0,0 @@ -import AVFoundation -import VideoToolbox - -class VideoCompressor { - private let url: URL - private let targetBitrate: Int - private let maxSize: Int - private let codecPref: String - private let frameRateCap: Int - private let jobId: Int - private let onProgress: (Int, Double) -> Void - private var isCancelled = false - - init( - url: URL, - targetBitrate: Int, - maxSize: Int, - codecPref: String, - frameRateCap: Int, - jobId: Int, - onProgress: @escaping (Int, Double) -> Void - ) { - self.url = url - self.targetBitrate = targetBitrate - self.maxSize = maxSize - self.codecPref = codecPref - self.frameRateCap = frameRateCap - self.jobId = jobId - self.onProgress = onProgress - } - - func cancel() { - isCancelled = true - } - - func compress() async throws -> [String: Any] { - let asset = AVURLAsset( - url: url, - options: [AVURLAssetPreferPreciseDurationAndTimingKey: true] - ) - let duration = try await asset.load(.duration) - let totalSeconds = CMTimeGetSeconds(duration) - guard totalSeconds > 0 else { throw err("Invalid video duration", code: 2) } - - let videoTracks = try await asset.loadTracks(withMediaType: .video) - guard let videoTrack = videoTracks.first else { - throw err("No video track found", code: 1) - } - - let naturalSize = try await videoTrack.load(.naturalSize) - let preferredTransform = try await videoTrack.load(.preferredTransform) - - let rotatedRect = CGRect(origin: .zero, size: naturalSize).applying(preferredTransform) - let displaySize = CGSize( - width: abs(rotatedRect.width), - height: abs(rotatedRect.height) - ) - let outputSize = scaleEvenly(displaySize: displaySize, maxSize: maxSize) - - let audioTracks = try await asset.loadTracks(withMediaType: .audio) - - // 'auto' targets h264 — server pipeline is HLS, which favors h264 (HEVC needs - // fMP4 segments + commercial licensing). HEVC remains opt-in via codec: 'hevc'. - let useHEVC: Bool - switch codecPref { - case "hevc": useHEVC = true - default: useHEVC = false - } - let codecType: AVVideoCodecType = useHEVC ? .hevc : .h264 - let profileLevel: String = useHEVC - ? kVTProfileLevel_HEVC_Main_AutoLevel as String - : kVTProfileLevel_H264_High_AutoLevel as String - - let effectiveBitrate = targetBitrate > 0 - ? targetBitrate - : (useHEVC ? 2_500_000 : 3_000_000) - - let outputURL = FileManager.default.temporaryDirectory - .appendingPathComponent(UUID().uuidString) - .appendingPathExtension("mp4") - - let videoComposition = makeRotatingComposition( - videoTrack: videoTrack, - preferredTransform: preferredTransform, - naturalSize: naturalSize, - outputSize: outputSize, - duration: duration - ) - - let reader = try AVAssetReader(asset: asset) - - let videoReaderSettings: [String: Any] = [ - kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA - ] - let videoReaderOutput = AVAssetReaderVideoCompositionOutput( - videoTracks: [videoTrack], - videoSettings: videoReaderSettings - ) - videoReaderOutput.videoComposition = videoComposition - videoReaderOutput.alwaysCopiesSampleData = false - guard reader.canAdd(videoReaderOutput) else { - throw err("Cannot read video track", code: 3) - } - reader.add(videoReaderOutput) - - var compressionProps: [String: Any] = [ - AVVideoAverageBitRateKey: effectiveBitrate, - AVVideoProfileLevelKey: profileLevel, - AVVideoMaxKeyFrameIntervalKey: max(frameRateCap * 3, 30), - AVVideoExpectedSourceFrameRateKey: frameRateCap, - AVVideoAllowFrameReorderingKey: false, - kVTCompressionPropertyKey_RealTime as String: true, - ] - let peakBytesPerSecond = Int(Double(effectiveBitrate) / 8.0 * 1.5) - compressionProps[kVTCompressionPropertyKey_DataRateLimits as String] = [ - peakBytesPerSecond, 1.0 - ] as CFArray - - let videoColorProps: [String: Any] = [ - AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, - AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, - AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2, - ] - - let videoWriterSettings: [String: Any] = [ - AVVideoCodecKey: codecType, - AVVideoWidthKey: outputSize.width, - AVVideoHeightKey: outputSize.height, - AVVideoColorPropertiesKey: videoColorProps, - AVVideoCompressionPropertiesKey: compressionProps, - ] - let videoWriterInput = AVAssetWriterInput( - mediaType: .video, - outputSettings: videoWriterSettings - ) - videoWriterInput.expectsMediaDataInRealTime = false - - let writer = try AVAssetWriter(outputURL: outputURL, fileType: .mp4) - writer.shouldOptimizeForNetworkUse = true - writer.metadata = [] - guard writer.canAdd(videoWriterInput) else { - throw err("Cannot write video track", code: 4) - } - writer.add(videoWriterInput) - - var audioReaderOutput: AVAssetReaderTrackOutput? - var audioWriterInput: AVAssetWriterInput? - if let audioTrack = audioTracks.first { - let audioDecoderSettings: [String: Any] = [ - AVFormatIDKey: kAudioFormatLinearPCM, - AVLinearPCMBitDepthKey: 16, - AVLinearPCMIsFloatKey: false, - AVLinearPCMIsBigEndianKey: false, - AVLinearPCMIsNonInterleaved: false, - ] - let audioOutput = AVAssetReaderTrackOutput( - track: audioTrack, - outputSettings: audioDecoderSettings - ) - audioOutput.alwaysCopiesSampleData = false - if reader.canAdd(audioOutput) { - reader.add(audioOutput) - audioReaderOutput = audioOutput - - let audioEncoderSettings: [String: Any] = [ - AVFormatIDKey: kAudioFormatMPEG4AAC, - AVSampleRateKey: 44100, - AVNumberOfChannelsKey: 2, - AVEncoderBitRateKey: 128_000, - ] - let audioInput = AVAssetWriterInput( - mediaType: .audio, - outputSettings: audioEncoderSettings - ) - audioInput.expectsMediaDataInRealTime = false - if writer.canAdd(audioInput) { - writer.add(audioInput) - audioWriterInput = audioInput - } - } - } - - guard reader.startReading() else { - throw reader.error ?? err("Reader failed to start", code: 8) - } - guard writer.startWriting() else { - throw writer.error ?? err("Writer failed to start", code: 9) - } - writer.startSession(atSourceTime: .zero) - - let minFrameIntervalSeconds = 1.0 / Double(frameRateCap) - let minFrameInterval = CMTime( - seconds: minFrameIntervalSeconds, - preferredTimescale: 600 - ) - - await withTaskGroup(of: Void.self) { group in - group.addTask { [self] in - await processVideoTrack( - readerOutput: videoReaderOutput, - writerInput: videoWriterInput, - totalDuration: totalSeconds, - minFrameInterval: minFrameInterval - ) - } - if let audioOutput = audioReaderOutput, let audioInput = audioWriterInput { - group.addTask { [self] in - await processAudioTrack( - readerOutput: audioOutput, - writerInput: audioInput - ) - } - } - await group.waitForAll() - } - - if isCancelled { - writer.cancelWriting() - try? FileManager.default.removeItem(at: outputURL) - throw err("Compression cancelled", code: 5) - } - - if reader.status == .failed { - let error = reader.error ?? err("Reader failed", code: 6) - writer.cancelWriting() - try? FileManager.default.removeItem(at: outputURL) - throw error - } - - await writer.finishWriting() - - if writer.status == .failed { - let error = writer.error ?? err("Writer failed", code: 7) - try? FileManager.default.removeItem(at: outputURL) - throw error - } - - onProgress(jobId, 1.0) - - let attributes = try FileManager.default.attributesOfItem(atPath: outputURL.path) - let fileSize = attributes[.size] as? Int ?? 0 - - return [ - "uri": outputURL.absoluteString, - "size": fileSize, - "mimeType": "video/mp4", - "width": outputSize.width, - "height": outputSize.height, - "duration": totalSeconds, - "codec": useHEVC ? "hevc" : "h264", - ] - } - - private func processVideoTrack( - readerOutput: AVAssetReaderOutput, - writerInput: AVAssetWriterInput, - totalDuration: Double, - minFrameInterval: CMTime - ) async { - var lastProgressTime: CFAbsoluteTime = 0 - var lastAppendedPTS: CMTime? - var finished = false - - await withCheckedContinuation { (continuation: CheckedContinuation) in - writerInput.requestMediaDataWhenReady( - on: DispatchQueue(label: "com.bsky.videocompress.video") - ) { - let finish = { - if !finished { - finished = true - writerInput.markAsFinished() - continuation.resume() - } - } - while writerInput.isReadyForMoreMediaData { - if finished { return } - if self.isCancelled { - finish() - return - } - guard let sampleBuffer = readerOutput.copyNextSampleBuffer() else { - finish() - return - } - - let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) - if let last = lastAppendedPTS { - let delta = CMTimeSubtract(pts, last) - if CMTimeCompare(delta, minFrameInterval) < 0 { - continue - } - } - lastAppendedPTS = pts - - if !writerInput.append(sampleBuffer) { - finish() - return - } - - let now = CFAbsoluteTimeGetCurrent() - if now - lastProgressTime >= 0.1 { - lastProgressTime = now - let progress = min(CMTimeGetSeconds(pts) / totalDuration, 1.0) - self.onProgress(self.jobId, progress) - } - } - } - } - } - - private func processAudioTrack( - readerOutput: AVAssetReaderOutput, - writerInput: AVAssetWriterInput - ) async { - var finished = false - - await withCheckedContinuation { (continuation: CheckedContinuation) in - writerInput.requestMediaDataWhenReady( - on: DispatchQueue(label: "com.bsky.videocompress.audio") - ) { - let finish = { - if !finished { - finished = true - writerInput.markAsFinished() - continuation.resume() - } - } - while writerInput.isReadyForMoreMediaData { - if finished { return } - if self.isCancelled { - finish() - return - } - guard let sampleBuffer = readerOutput.copyNextSampleBuffer() else { - finish() - return - } - if !writerInput.append(sampleBuffer) { - finish() - return - } - } - } - } - } - - private func makeRotatingComposition( - videoTrack: AVAssetTrack, - preferredTransform: CGAffineTransform, - naturalSize: CGSize, - outputSize: (width: Int, height: Int), - duration: CMTime - ) -> AVMutableVideoComposition { - let composition = AVMutableVideoComposition() - composition.renderSize = CGSize(width: outputSize.width, height: outputSize.height) - composition.frameDuration = CMTime(value: 1, timescale: Int32(frameRateCap)) - - let rotatedRect = CGRect(origin: .zero, size: naturalSize).applying(preferredTransform) - let translate = CGAffineTransform( - translationX: -rotatedRect.minX, - y: -rotatedRect.minY - ) - let displaySize = CGSize( - width: abs(rotatedRect.width), - height: abs(rotatedRect.height) - ) - let scaleX = CGFloat(outputSize.width) / displaySize.width - let scaleY = CGFloat(outputSize.height) / displaySize.height - let scale = CGAffineTransform(scaleX: scaleX, y: scaleY) - let combined = preferredTransform.concatenating(translate).concatenating(scale) - - let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack) - layerInstruction.setTransform(combined, at: .zero) - - let instruction = AVMutableVideoCompositionInstruction() - instruction.timeRange = CMTimeRange(start: .zero, duration: duration) - instruction.layerInstructions = [layerInstruction] - - composition.instructions = [instruction] - return composition - } - - private func scaleEvenly(displaySize: CGSize, maxSize: Int) -> (width: Int, height: Int) { - let cap = CGFloat(maxSize) - let scale: CGFloat - if displaySize.width <= cap && displaySize.height <= cap { - scale = 1.0 - } else if displaySize.width > displaySize.height { - scale = cap / displaySize.width - } else { - scale = cap / displaySize.height - } - return ( - roundToEven(Int(displaySize.width * scale)), - roundToEven(Int(displaySize.height * scale)) - ) - } - - private func roundToEven(_ value: Int) -> Int { - return value % 2 == 0 ? value : value - 1 - } - - private func err(_ message: String, code: Int) -> NSError { - return NSError( - domain: "ExpoBlueskyVideoCompress", - code: code, - userInfo: [NSLocalizedDescriptionKey: message] - ) - } -} diff --git a/modules/expo-bluesky-video-compress/ios/VideoProber.swift b/modules/expo-bluesky-video-compress/ios/VideoProber.swift deleted file mode 100644 index 4636ed7d3c..0000000000 --- a/modules/expo-bluesky-video-compress/ios/VideoProber.swift +++ /dev/null @@ -1,105 +0,0 @@ -import AVFoundation -import UniformTypeIdentifiers - -struct VideoProber { - static func probe(url: URL) async throws -> [String: Any] { - let asset = AVURLAsset(url: url) - - let duration = try await asset.load(.duration) - let tracks = try await asset.loadTracks(withMediaType: .video) - - guard let videoTrack = tracks.first else { - throw NSError( - domain: "ExpoBlueskyVideoCompress", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "No video track found"] - ) - } - - let naturalSize = try await videoTrack.load(.naturalSize) - let preferredTransform = try await videoTrack.load(.preferredTransform) - let estimatedDataRate = try await videoTrack.load(.estimatedDataRate) - let nominalFrameRate = try await videoTrack.load(.nominalFrameRate) - let formatDescriptions = try await videoTrack.load(.formatDescriptions) - - var codec = "unknown" - var isHDR = false - if let formatDescription = formatDescriptions.first { - let subType = CMFormatDescriptionGetMediaSubType(formatDescription) - codec = fourCCToString(subType) - // Dolby Vision codecs are HDR by definition. - let dolbyVisionSubtypes: Set = ["dvhe", "dvh1", "dvav", "dva1"] - if dolbyVisionSubtypes.contains(codec) { - isHDR = true - } else if let extensions = CMFormatDescriptionGetExtensions(formatDescription) - as? [String: Any] - { - let transferKey = kCMFormatDescriptionExtension_TransferFunction as String - if let transfer = extensions[transferKey] as? String { - let hlg = kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG as String - let pq = kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ as String - isHDR = transfer == hlg || transfer == pq - } - } - } - - let rotation = rotationFromTransform(preferredTransform) - let isRotated = rotation == 90 || rotation == 270 - let width = isRotated ? Int(naturalSize.height) : Int(naturalSize.width) - let height = isRotated ? Int(naturalSize.width) : Int(naturalSize.height) - - let audioTracks = try await asset.loadTracks(withMediaType: .audio) - let hasAudio = !audioTracks.isEmpty - - let fileSize: Int - if let attributes = try? FileManager.default.attributesOfItem(atPath: url.path), - let size = attributes[.size] as? Int { - fileSize = size - } else { - fileSize = 0 - } - - let mimeType: String - if let utType = UTType(filenameExtension: url.pathExtension) { - mimeType = utType.preferredMIMEType ?? "video/mp4" - } else { - mimeType = "video/mp4" - } - - let durationSeconds = CMTimeGetSeconds(duration) - var bitrate = Int(estimatedDataRate) - if bitrate == 0 && durationSeconds > 0 { - bitrate = Int(Double(fileSize * 8) / durationSeconds) - } - - return [ - "width": width, - "height": height, - "duration": durationSeconds, - "bitrate": bitrate, - "fileSize": fileSize, - "mimeType": mimeType, - "codec": codec, - "hasAudio": hasAudio, - "frameRate": nominalFrameRate, - "rotation": rotation, - "isHDR": isHDR - ] - } - - private static func rotationFromTransform(_ transform: CGAffineTransform) -> Int { - let angle = atan2(transform.b, transform.a) - let degrees = Int(round(angle * 180.0 / .pi)) - return ((degrees % 360) + 360) % 360 - } - - private static func fourCCToString(_ code: FourCharCode) -> String { - let chars: [Character] = [ - Character(UnicodeScalar((code >> 24) & 0xFF)!), - Character(UnicodeScalar((code >> 16) & 0xFF)!), - Character(UnicodeScalar((code >> 8) & 0xFF)!), - Character(UnicodeScalar(code & 0xFF)!) - ] - return String(chars).trimmingCharacters(in: .whitespaces) - } -} diff --git a/modules/expo-bluesky-video-compress/src/ExpoBlueskyVideoCompressModule.ts b/modules/expo-bluesky-video-compress/src/ExpoBlueskyVideoCompressModule.ts deleted file mode 100644 index dcf63c729f..0000000000 --- a/modules/expo-bluesky-video-compress/src/ExpoBlueskyVideoCompressModule.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {type EventSubscription, requireNativeModule} from 'expo-modules-core' - -import { - type CompressResult, - type NativeCompressOptions, - type VideoMetadata, -} from './types' - -type ProgressEvent = {id: number; progress: number} - -interface ExpoBlueskyVideoCompressModule { - probe(uri: string): Promise - compress(uri: string, options: NativeCompressOptions): Promise - cancel(jobId: number): void - addListener( - eventName: 'onProgress', - listener: (event: ProgressEvent) => void, - ): EventSubscription -} - -export default requireNativeModule( - 'ExpoBlueskyVideoCompress', -) diff --git a/modules/expo-bluesky-video-compress/src/types.ts b/modules/expo-bluesky-video-compress/src/types.ts deleted file mode 100644 index df9b9ed1ac..0000000000 --- a/modules/expo-bluesky-video-compress/src/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -export type CodecPreference = 'auto' | 'hevc' | 'h264' - -export type VideoMetadata = { - width: number - height: number - duration: number - bitrate: number - fileSize: number - mimeType: string - codec: string - hasAudio: boolean - frameRate: number - rotation: number - isHDR: boolean -} - -export type CompressOptions = { - targetBitrate?: number - maxSize?: number - codec?: CodecPreference - frameRateCap?: number -} - -export type CompressCallbacks = { - onProgress?: (progress: number) => void - signal?: AbortSignal -} - -export type CompressResult = { - uri: string - size: number - mimeType: string - width: number - height: number - duration: number - codec: 'h264' | 'hevc' -} - -export type NativeCompressOptions = { - targetBitrate: number - maxSize: number - codec: CodecPreference - frameRateCap: number - jobId: number -} diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index bef12f4397..e0435c5a4c 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -296,19 +296,6 @@ "count": 1 } }, - "src/components/StarterPack/Main/ProfilesList.tsx": { - "typescript/no-misused-promises": { - "count": 1 - } - }, - "src/components/StarterPack/ProfileStarterPacks.tsx": { - "typescript/no-explicit-any": { - "count": 1 - }, - "typescript/no-misused-promises": { - "count": 2 - } - }, "src/components/StarterPack/QrCodeDialog.tsx": { "typescript/no-floating-promises": { "count": 5 @@ -317,14 +304,6 @@ "count": 4 } }, - "src/components/StarterPack/ShareDialog.tsx": { - "typescript/no-floating-promises": { - "count": 1 - }, - "typescript/require-await": { - "count": 1 - } - }, "src/components/StarterPack/Wizard/WizardEditListDialog.tsx": { "typescript/no-explicit-any": { "count": 1 @@ -406,21 +385,11 @@ "count": 1 } }, - "src/components/dialogs/StarterPackDialog.tsx": { - "typescript/no-misused-promises": { - "count": 1 - } - }, "src/components/dialogs/SwitchAccount.tsx": { "typescript/no-floating-promises": { "count": 1 } }, - "src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx": { - "typescript/no-floating-promises": { - "count": 2 - } - }, "src/components/dialogs/lists/CreateOrEditListDialog.tsx": { "typescript/no-explicit-any": { "count": 2 @@ -557,11 +526,6 @@ "count": 2 } }, - "src/lib/async/until.ts": { - "typescript/no-explicit-any": { - "count": 2 - } - }, "src/lib/broadcast/stub.ts": { "typescript/no-explicit-any": { "count": 1 @@ -674,7 +638,7 @@ "count": 1 }, "typescript/no-floating-promises": { - "count": 5 + "count": 3 } }, "src/lib/media/manip.web.ts": { @@ -870,14 +834,6 @@ "count": 1 } }, - "src/screens/ProfileList/components/MoreOptionsMenu.tsx": { - "typescript/no-floating-promises": { - "count": 1 - }, - "typescript/no-misused-promises": { - "count": 5 - } - }, "src/screens/ProfileList/components/SubscribeMenu.tsx": { "typescript/no-misused-promises": { "count": 2 @@ -1009,22 +965,6 @@ "count": 1 } }, - "src/screens/StarterPack/StarterPackScreen.tsx": { - "typescript/no-floating-promises": { - "count": 1 - }, - "typescript/require-await": { - "count": 1 - } - }, - "src/screens/StarterPack/Wizard/index.tsx": { - "typescript/no-floating-promises": { - "count": 2 - }, - "typescript/require-await": { - "count": 1 - } - }, "src/state/a11y.tsx": { "typescript/no-floating-promises": { "count": 1 @@ -1191,7 +1131,7 @@ "count": 2 }, "typescript/no-floating-promises": { - "count": 2 + "count": 1 }, "typescript/no-unsafe-member-access": { "count": 2 @@ -1226,7 +1166,7 @@ "count": 3 }, "typescript/no-floating-promises": { - "count": 3 + "count": 2 }, "typescript/no-unsafe-member-access": { "count": 3 @@ -1246,11 +1186,6 @@ "count": 2 } }, - "src/state/queries/starter-packs.ts": { - "typescript/require-await": { - "count": 1 - } - }, "src/state/queries/suggested-follows.ts": { "no-unused-vars": { "count": 1 @@ -1318,11 +1253,6 @@ "count": 1 } }, - "src/view/com/composer/drafts/DraftsButton.tsx": { - "typescript/no-misused-promises": { - "count": 1 - } - }, "src/view/com/composer/drafts/state/queries.ts": { "typescript/no-explicit-any": { "count": 2 @@ -1533,11 +1463,6 @@ "count": 1 } }, - "src/view/screens/Home.tsx": { - "typescript/no-floating-promises": { - "count": 1 - } - }, "src/view/screens/ModerationBlockedAccounts.tsx": { "typescript/no-misused-promises": { "count": 3 diff --git a/package.json b/package.json index 20d2ea850c..c3c55f128c 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "@bsky.app/expo-scroll-edge-effect": "^0.1.9", "@bsky.app/expo-translate-text": "^0.2.9", "@bsky.app/peek-menu": "^0.3.2", - "@bsky.app/react-native-uitextview": "^2.7.0", + "@bsky.app/react-native-uitextview": "^2.7.1", "@bsky.app/sift": "^0.3.9", "@bsky.app/tapper": "^0.6.1", "@bsky.app/video": "0.3.6", @@ -159,6 +159,7 @@ "babel-plugin-transform-remove-console": "^6.9.4", "bcp-47": "^2.1.0", "bcp-47-match": "^2.0.3", + "bidi-js": "^1.0.3", "date-fns": "^4.4.0", "email-validator": "^2.0.4", "emoji-mart": "^5.6.0", @@ -229,7 +230,6 @@ "react-is": "19", "react-keyed-flatten-children": "^5.0.0", "react-native": "0.86.0", - "react-native-compressor": "1.13.0", "react-native-date-picker": "^5.0.13", "react-native-device-attest": "^0.1.6", "react-native-drawer-layout": "^4.2.3", @@ -240,7 +240,7 @@ "react-native-pager-view": "6.8.0", "react-native-progress": "^5.0.1", "react-native-qrcode-styled": "^0.3.3", - "react-native-reanimated": "~4.5.3", + "react-native-reanimated": "~4.6.0", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.26.2", "react-native-scroll-forwarder": "link:./modules/react-native-scroll-forwarder", @@ -250,7 +250,7 @@ "react-native-web": "^0.21.0", "react-native-web-webview": "^1.0.2", "react-native-webview": "^13.16.1", - "react-native-worklets": "0.11.3", + "react-native-worklets": "0.12.1", "react-remove-scroll-bar": "^2.3.8", "react-responsive": "^10.0.1", "react-textarea-autosize": "^8.5.3", @@ -305,6 +305,7 @@ "prettier": "3.9.6", "react-native-dotenv": "^3.4.11", "react-refresh": "^0.14.0", + "sharp": "^0.35.4", "svgo": "^4.0.2", "ts-plugin-sort-import-suggestions": "^1.0.4", "typescript": "npm:@typescript/typescript6@^6.0.2", diff --git a/patches/@formatjs__bigdecimal@0.2.5.patch b/patches/@formatjs__bigdecimal@0.2.5.patch new file mode 100644 index 0000000000..bdd3a99bc6 --- /dev/null +++ b/patches/@formatjs__bigdecimal@0.2.5.patch @@ -0,0 +1,30 @@ +diff --git a/index.js b/index.js +index 12101e1b954e1a8c7bf7887bef4f4e08fb06a8be..51d7af352a8a222c5320426f376d51c5a173300b 100644 +--- a/index.js ++++ b/index.js +@@ -170,6 +170,13 @@ var BigDecimal = class BigDecimal { + times(y) { + const other = BigDecimal._coerce(y); + if (this._special || other._special) return this._specialArith(other, "times"); ++ // Fast path: multiplying by exactly 1 is an identity operation. ECMA-402's ++ // ToRawFixed multiplies by roundingIncrement, which defaults to 1, on every ++ // format call - without this check that is a full BigInt multiply followed ++ // by a trailing-zero strip. BigDecimal is immutable, so returning the ++ // existing instance is safe. ++ if (other._mantissa === 1n && other._exponent === 0) return this; ++ if (this._mantissa === 1n && this._exponent === 0) return other; + if (this._mantissa === 0n || other._mantissa === 0n) { + const negZero = this._isSignNegative() ? !other._isSignNegative() : other._isSignNegative(); + return BigDecimal._create(0n, 0, SpecialValue.NONE, negZero); +@@ -189,6 +196,11 @@ var BigDecimal = class BigDecimal { + const negZero = this._isSignNegative() !== other._isSignNegative(); + return BigDecimal._create(0n, 0, SpecialValue.NONE, negZero); + } ++ // Fast path: dividing by exactly 1 is an identity operation. ECMA-402's ++ // ToRawFixed divides by roundingIncrement, which defaults to 1, on every ++ // format call - without this check that is a 10^DIV_PRECISION BigInt ++ // scale-up, a division, and a trailing-zero strip that undoes the scaling. ++ if (other._mantissa === 1n && other._exponent === 0) return this; + const [nm, ne] = removeTrailingZeros(this._mantissa * bigintPow10(DIV_PRECISION) / other._mantissa, this._exponent - other._exponent - DIV_PRECISION); + return BigDecimal._create(nm, ne, SpecialValue.NONE, false); + } diff --git a/patches/expo-modules-core@57.0.8.patch b/patches/expo-modules-core@57.0.8.patch index 7de1337aac..de78938be9 100644 --- a/patches/expo-modules-core@57.0.8.patch +++ b/patches/expo-modules-core@57.0.8.patch @@ -1,3 +1,19 @@ +diff --git a/android/src/main/cpp/worklets/WorkletJSCallInvoker.cpp b/android/src/main/cpp/worklets/WorkletJSCallInvoker.cpp +index a2d707ce76ceb35456eb075d06473ac98ad8b5eb..be2e3d66031d2bbd6208a7cdc1700a9daefb1267 100644 +--- a/android/src/main/cpp/worklets/WorkletJSCallInvoker.cpp ++++ b/android/src/main/cpp/worklets/WorkletJSCallInvoker.cpp +@@ -24,10 +24,7 @@ namespace expo { + return; + } + +- workletRuntime->executeSync([func = std::move(func)](jsi::Runtime &rt) -> jsi::Value { +- func(rt); +- return jsi::Value::undefined(); +- }); ++ workletRuntime->runSync(func); + } + } // namespace expo + diff --git a/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt index 47c4d15f6b10bbd77858cfff425cda9a618735b9..afe138d22d566244482498a7be0e14b8454eab96 100644 --- a/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt @@ -13,3 +29,16 @@ index 47c4d15f6b10bbd77858cfff425cda9a618735b9..afe138d22d566244482498a7be0e14b8 // Check for Content-Type val skipContentTypes = listOf( "text/event-stream", // Server Sent Events +diff --git a/ios/WorkletsAdapter/ExpoWorkletsBridgeProvider.mm b/ios/WorkletsAdapter/ExpoWorkletsBridgeProvider.mm +index 126545fae12dc3af71e0ea382b976ef430e62d17..d97163a2e9a9db09f43786bc919f2fa229f21901 100644 +--- a/ios/WorkletsAdapter/ExpoWorkletsBridgeProvider.mm ++++ b/ios/WorkletsAdapter/ExpoWorkletsBridgeProvider.mm +@@ -233,7 +233,7 @@ - (void)executeWorkletWithRuntimeHandle:(id)runtimeHandle + return; + } + +- workletRuntime->executeSync([worklet, arguments](jsi::Runtime &rt) -> jsi::Value { ++ workletRuntime->runSync([worklet, arguments](jsi::Runtime &rt) -> jsi::Value { + return callWorklet(rt, worklet, arguments); + }); + } diff --git a/patches/expo-modules-core@57.0.8.patch.md b/patches/expo-modules-core@57.0.8.patch.md index 56ba6d08bf..3c03b7d93f 100644 --- a/patches/expo-modules-core@57.0.8.patch.md +++ b/patches/expo-modules-core@57.0.8.patch.md @@ -3,3 +3,15 @@ ### Android: bitdrift interceptor Fixes an issue where bitdrift's API stream gets blocked by the Expo interceptor used to power the devtools. + +### iOS + Android: worklets `runSync` migration + +Backport of https://github.com/expo/expo/pull/49366 ("[sdk-57] Backport +WorkletRuntime runSync migration"). react-native-worklets 0.12 removed the +deprecated `WorkletRuntime::executeSync`, so the worklets adapters in +`ios/WorkletsAdapter/ExpoWorkletsBridgeProvider.mm` and +`android/src/main/cpp/worklets/WorkletJSCallInvoker.cpp` fail to compile +against it. The patch swaps both call sites to `runSync` (available since +worklets 0.7.0). Required for the react-native-reanimated 4.6.0 / +react-native-worklets 0.12.1 upgrade; drop once expo-modules-core ships a +version containing that PR. diff --git a/patches/react-native-compressor@1.13.0.patch b/patches/react-native-compressor@1.13.0.patch deleted file mode 100644 index 1a0f4c61d7..0000000000 --- a/patches/react-native-compressor@1.13.0.patch +++ /dev/null @@ -1,59 +0,0 @@ -diff --git a/android/build.gradle b/android/build.gradle -index 5071139f8ee5fbba085d2afe3b2093de8eda915c..84bee34a238c6510169f6b6bdb0fda0594c77136 100644 ---- a/android/build.gradle -+++ b/android/build.gradle -@@ -115,7 +115,6 @@ dependencies { - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" - implementation 'org.mp4parser:isoparser:1.9.56' -- implementation 'com.github.banketree:AndroidLame-kotlin:v0.0.1' - implementation 'javazoom:jlayer:1.0.1' - } - -diff --git a/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt b/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt -deleted file mode 100644 -index 9292d3ee50776bd9d7760b8dcf6d123d44b4e31b..0000000000000000000000000000000000000000 -diff --git a/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt b/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt -deleted file mode 100644 -index c6551828014437a14dc8f2f19488b647dba1bbe1..0000000000000000000000000000000000000000 -diff --git a/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt b/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt -deleted file mode 100644 -index 42040b4916573463415ef2f57789b3c4fa25d135..0000000000000000000000000000000000000000 -diff --git a/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt b/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt -index 446d4fb8b69e7cfdb51b29603aa2d52aac1ab8c8..f02190992dac823b6bbf2d77880a25adc48f16c7 100644 ---- a/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt -+++ b/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt -@@ -11,7 +11,9 @@ class AudioMain(private val reactContext: ReactApplicationContext) { - promise: Promise) { - try { - -- AudioCompressor.CompressAudio(fileUrl,optionMap,reactContext,promise) -+ // Skip compression on Android to avoid libandroidlame dependency -+ // Return the original file URL without compression -+ promise.resolve(fileUrl) - } catch (ex: Exception) { - promise.reject(ex) - } -diff --git a/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt b/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt -index c14b727e930f4114765bfbe15b742ddcdeaa392f..1198908fcc66eeeea5e537085d7632a0d4b04545 100644 ---- a/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt -+++ b/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt -@@ -7,7 +7,6 @@ import android.provider.OpenableColumns - import android.util.Log - import com.facebook.react.bridge.Promise - import com.facebook.react.bridge.ReactApplicationContext --import com.reactnativecompressor.Audio.AudioCompressor - import com.reactnativecompressor.Video.VideoCompressor.CompressionListener - import com.reactnativecompressor.Video.VideoCompressor.VideoCompressorClass - import java.io.FileNotFoundException -@@ -152,10 +151,6 @@ object Utils { - } - } - -- fun addLog(log: String) { -- Log.d(AudioCompressor.TAG, log) -- } -- - val exifAttributes = arrayOf( - "FNumber", - "ApertureValue", diff --git a/patches/react-native-compressor@1.13.0.patch.md b/patches/react-native-compressor@1.13.0.patch.md deleted file mode 100644 index de59e55244..0000000000 --- a/patches/react-native-compressor@1.13.0.patch.md +++ /dev/null @@ -1,5 +0,0 @@ -# react-native-compressor - -Patch file taken from https://github.com/numandev1/react-native-compressor/pull/355#issuecomment-3180870738 - -This patch removes the audio compression feature on Android from the library. This is because `libandroidlame.so`, the native dependency, does not support 16kb page sizes, and the Play Store has made this mandatory as of 1st Nov 2025. diff --git a/patches/react-native-reanimated@4.5.3.patch b/patches/react-native-reanimated@4.5.3.patch deleted file mode 100644 index 859f9bc2b4..0000000000 --- a/patches/react-native-reanimated@4.5.3.patch +++ /dev/null @@ -1,283 +0,0 @@ -diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h -index 8603591..20d042b 100644 ---- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h -+++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h -@@ -62,11 +62,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele - const SharedComponentDescriptorRegistry &componentDescriptorRegistry, - const std::shared_ptr &contextContainer, - jsi::Runtime &uiRuntime, -- const std::shared_ptr &uiScheduler -+ const std::shared_ptr &uiScheduler, -+ const std::shared_ptr &uiManager - #ifdef ANDROID - , - const PreserveMountedTagsFunction &filterUnmountedTagsFunction, -- const std::shared_ptr &uiManager, - const std::shared_ptr &jsInvoker - #endif - ) -@@ -74,11 +74,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele - contextContainer_(contextContainer), - componentDescriptorRegistry_(componentDescriptorRegistry), - uiRuntime_(uiRuntime), -- uiScheduler_(uiScheduler) -+ uiScheduler_(uiScheduler), -+ uiManager_(uiManager) - #ifdef ANDROID - , - preserveMountedTags_(filterUnmountedTagsFunction), -- uiManager_(uiManager), - jsInvoker_(jsInvoker) - #endif - { -@@ -98,10 +98,10 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele - SharedComponentDescriptorRegistry componentDescriptorRegistry_; - jsi::Runtime &uiRuntime_; - const std::shared_ptr uiScheduler_; -+ std::shared_ptr uiManager_; - PreserveMountedTagsFunction preserveMountedTags_; - - #ifdef ANDROID -- std::shared_ptr uiManager_; - std::shared_ptr jsInvoker_; - - void restoreOpacityInCaseOfFlakyEnteringAnimation(SurfaceId surfaceId) const; -diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h -index fcc677f..115971a 100644 ---- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h -+++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h -@@ -67,11 +67,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon, - const SharedComponentDescriptorRegistry &componentDescriptorRegistry, - const std::shared_ptr &contextContainer, - jsi::Runtime &uiRuntime, -- const std::shared_ptr &uiScheduler -+ const std::shared_ptr &uiScheduler, -+ const std::shared_ptr &uiManager - #ifdef ANDROID - , - const PreserveMountedTagsFunction &filterUnmountedTagsFunction, -- const std::shared_ptr &uiManager, - const std::shared_ptr &jsInvoker - #endif - ) -@@ -80,11 +80,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon, - componentDescriptorRegistry, - contextContainer, - uiRuntime, -- uiScheduler -+ uiScheduler, -+ uiManager - #ifdef ANDROID - , - filterUnmountedTagsFunction, -- uiManager, - jsInvoker - #endif - ), -diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp -index df53d8d..735f138 100644 ---- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp -+++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp -@@ -1,6 +1,7 @@ - #include - - #include -+#include - #include - - #include -@@ -60,14 +61,37 @@ std::optional LayoutAnimationsProxy_Legacy::pullTransaction - - parseRemoveMutations(movedViews, mutations, roots); - -- auto shouldAnimate = !surfacesToRemove_.contains(surfaceId); -- surfacesToRemove_.erase(surfaceId); -+ // Consume the teardown mark only on the transaction that actually clears -+ // the root — pulls emitted for animation frames must not eat it early. -+ auto shouldAnimate = true; -+ const auto removesRootChildren = std::ranges::any_of(mutations, [surfaceId](const auto &mutation) { -+ return mutation.type == ShadowViewMutation::Remove && mutation.parentTag == surfaceId; -+ }); -+ if (removesRootChildren) { -+ shouldAnimate = surfacesToRemove_.erase(surfaceId) == 0; -+ } - handleRemovals(filteredMutations, roots, deadNodes, shouldAnimate); - - handleUpdatesAndEnterings(filteredMutations, movedViews, mutations, propsParserContext, surfaceId); - - addOngoingAnimations(surfaceId, filteredMutations); - -+ // The LayoutAnimationDriver can emit a final keyframe update in the same -+ // transaction as the deferred Remove/Delete it withheld for a delete -+ // animation. We emit removals before updates, so such an update would -+ // otherwise reach the mounting layer after its view was deleted. -+ std::unordered_set deletedTags; -+ for (const auto &mutation : filteredMutations) { -+ if (mutation.type == ShadowViewMutation::Delete) { -+ deletedTags.insert(mutation.oldChildShadowView.tag); -+ } -+ } -+ if (!deletedTags.empty()) { -+ std::erase_if(filteredMutations, [&deletedTags](const auto &mutation) { -+ return mutation.type == ShadowViewMutation::Update && deletedTags.contains(mutation.newChildShadowView.tag); -+ }); -+ } -+ - return MountingTransaction{surfaceId, transactionNumber, std::move(filteredMutations), telemetry}; - } - -@@ -998,23 +1022,22 @@ inline bool MutationNode::isMutationNode() { - return true; - } - --// UIManagerAnimationDelegate -- --void LayoutAnimationsProxy_Legacy::uiManagerDidConfigureNextLayoutAnimation( -- jsi::Runtime &runtime, -- const RawValue &config, -- const jsi::Value &successCallbackValue, -- const jsi::Value &failureCallbackValue) const {} -+// UIManagerCommitHook - --void LayoutAnimationsProxy_Legacy::setComponentDescriptorRegistry( -- const SharedComponentDescriptorRegistry &componentDescriptorRegistry) {} -- --bool LayoutAnimationsProxy_Legacy::shouldAnimateFrame() const { -- return false; --} -- --void LayoutAnimationsProxy_Legacy::stopSurface(SurfaceId surfaceId) { -- surfacesToRemove_.insert(surfaceId); -+// Surface teardown commits an empty root (SurfaceHandler::stop) before the -+// teardown transaction is pulled — mark it so pullTransaction skips exit -+// animations. Reading the ShadowTreeRegistry here instead would deadlock (#8579). -+RootShadowNode::Unshared LayoutAnimationsProxy_Legacy::shadowTreeWillCommit( -+ const ShadowTree &shadowTree, -+ const RootShadowNode::Shared & /*oldRootShadowNode*/, -+ const RootShadowNode::Unshared &newRootShadowNode) noexcept { -+ auto lock = std::unique_lock(mutex); -+ if (newRootShadowNode->getChildren().empty()) { -+ surfacesToRemove_.insert(shadowTree.getSurfaceId()); -+ } else { -+ surfacesToRemove_.erase(shadowTree.getSurfaceId()); -+ } -+ return newRootShadowNode; - } - - } // namespace reanimated -diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h -index 57cc134..1a2966c 100644 ---- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h -+++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h -@@ -3,8 +3,8 @@ - #include - #include - #include --#include - #include -+#include - #include - #include - #include -@@ -102,7 +102,7 @@ struct SurfaceContext { - }; - - struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, -- public UIManagerAnimationDelegate, -+ public UIManagerCommitHook, - public std::enable_shared_from_this { - mutable std::unordered_map> nodeForTag_; - mutable std::recursive_mutex mutex; -@@ -116,11 +116,11 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, - const SharedComponentDescriptorRegistry &componentDescriptorRegistry, - const std::shared_ptr &contextContainer, - jsi::Runtime &uiRuntime, -- const std::shared_ptr &uiScheduler -+ const std::shared_ptr &uiScheduler, -+ const std::shared_ptr &uiManager - #ifdef ANDROID - , - const PreserveMountedTagsFunction &filterUnmountedTagsFunction, -- const std::shared_ptr &uiManager, - const std::shared_ptr &jsInvoker - #endif - ) -@@ -129,14 +129,19 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, - componentDescriptorRegistry, - contextContainer, - uiRuntime, -- uiScheduler -+ uiScheduler, -+ uiManager - #ifdef ANDROID - , - filterUnmountedTagsFunction, -- uiManager, - jsInvoker - #endif - ) { -+ uiManager->registerCommitHook(*this); -+ } -+ -+ ~LayoutAnimationsProxy_Legacy() override { -+ uiManager_->unregisterCommitHook(*this); - } - - void startEnteringAnimation(const int tag, ShadowViewMutation &mutation) const; -@@ -206,19 +211,15 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, - const TransactionTelemetry &telemetry, - ShadowViewMutationList mutations) const override; - -- // UIManagerAnimationDelegate -- -- void uiManagerDidConfigureNextLayoutAnimation( -- jsi::Runtime &runtime, -- const RawValue &config, -- const jsi::Value &successCallbackValue, -- const jsi::Value &failureCallbackValue) const override; -- -- void setComponentDescriptorRegistry(const SharedComponentDescriptorRegistry &componentDescriptorRegistry) override; -+ // UIManagerCommitHook - -- bool shouldAnimateFrame() const override; -+ void commitHookWasRegistered(const UIManager &uiManager) noexcept override {} -+ void commitHookWasUnregistered(const UIManager &uiManager) noexcept override {} - -- void stopSurface(SurfaceId surfaceId) override; -+ RootShadowNode::Unshared shadowTreeWillCommit( -+ const ShadowTree &shadowTree, -+ const RootShadowNode::Shared &oldRootShadowNode, -+ const RootShadowNode::Unshared &newRootShadowNode) noexcept override; - }; - - } // namespace reanimated -diff --git a/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp -index 2b68ff7..d08b1ae 100644 ---- a/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp -+++ b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp -@@ -1235,22 +1235,19 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() { - #endif - layoutAnimationsProxy_ = std::move(layoutAnimationsProxyExperimental); - } else { -- auto layoutAnimationsProxyLegacy = std::make_shared( -+ layoutAnimationsProxy_ = std::make_shared( - layoutAnimationsManager_, - componentDescriptorRegistry, - scheduler->getContextContainer(), - getJSIRuntimeFromWorkletRuntime(uiRuntime_), -- uiScheduler_ -+ uiScheduler_, -+ uiManager_ - #ifdef ANDROID - , - filterUnmountedTagsFunction_, -- uiManager_, - jsInvoker_ - #endif - ); -- // TODO (future): support in experimental -- uiManager_->setAnimationDelegate(layoutAnimationsProxyLegacy.get()); -- layoutAnimationsProxy_ = std::move(layoutAnimationsProxyLegacy); - } - } - } diff --git a/patches/react-native-reanimated@4.5.3.patch.md b/patches/react-native-reanimated@4.5.3.patch.md deleted file mode 100644 index f01428d09a..0000000000 --- a/patches/react-native-reanimated@4.5.3.patch.md +++ /dev/null @@ -1,27 +0,0 @@ -# react-native-reanimated@4.5.3.patch - -Backport of https://github.com/software-mansion/react-native-reanimated/pull/9901 -("refactor(LayoutAnimations): stop taking over UIManagerAnimationDelegate"). - -Reanimated's legacy `LayoutAnimationsProxy_Legacy` registered itself as the -`UIManagerAnimationDelegate` only to receive `stopSurface`. Occupying that slot -overwrites the `LayoutAnimationDriver` that React Native installs there, which -silently breaks `LayoutAnimation.configureNext` for the whole app. - -The patch makes the proxy detect surface teardown itself via a -`UIManagerCommitHook` (a commit with an empty root marks the surface in -`surfacesToRemove_`), frees the animation-delegate slot, and drops final -keyframe `Update` mutations for views deleted in the same transaction (a -deterministic `configureNext` delete-animation crash found in this app). -`uiManager` moves from Android-only to shared constructor args since the hook -registration needs it on both platforms. - -Only the `packages/react-native-reanimated` part of the PR is included (the -`apps/fabric-example` hunk is not part of the published package), and the hunks -were rebased onto the 4.5.3 release sources. - -Note that upstream's own `pullTransaction` rework in 4.5.3 (the new -`reconcileContradictedRemovals`) covers a different case - a `Create`/`Insert` -contradicting a *withheld* exit removal - and does not subsume the deleted-tag -`Update` filter here, which guards against the `LayoutAnimationDriver` final -keyframe. That driver only runs at all once this patch frees the delegate slot. diff --git a/patches/react-native-worklets@0.11.3.patch b/patches/react-native-worklets@0.11.3.patch deleted file mode 100644 index 3b85b954f7..0000000000 --- a/patches/react-native-worklets@0.11.3.patch +++ /dev/null @@ -1,88 +0,0 @@ -diff --git a/lib/module/threads.js b/lib/module/threads.js -index c17e314..71f3cf7 100644 ---- a/lib/module/threads.js -+++ b/lib/module/threads.js -@@ -1,7 +1,6 @@ - 'use strict'; - - import { IS_JEST } from './platformChecker'; --import { mockedRequestAnimationFrame } from "./runLoop/uiRuntime/mockedRequestAnimationFrame.js"; - export function scheduleOnUI(worklet, ...args) { - enqueueUI(worklet, args); - } -@@ -23,38 +22,50 @@ export function scheduleOnRN(fun, ...args) { - queueMicrotask(args.length ? () => fun(...args) : fun); - } - export function runOnUIAsync(worklet, ...args) { -- return new Promise(resolve => { -- enqueueUI(worklet, args, resolve); -+ return new Promise((resolve, reject) => { -+ enqueueUI(worklet, args, resolve, reject); - }); - } - let runOnUIQueue = []; --function enqueueUI(worklet, args, resolve) { -- if (IS_JEST) { -- mockedRequestAnimationFrame(() => { -- const result = worklet(...args); -- resolve?.(result); -- }); -- } else { -- const job = [worklet, args, resolve]; -- runOnUIQueue.push(job); -- if (runOnUIQueue.length === 1) { -+function enqueueUI(worklet, args, resolve, reject) { -+ const job = [worklet, args, resolve, reject]; -+ runOnUIQueue.push(job); -+ if (runOnUIQueue.length === 1) { -+ if (IS_JEST) { - flushUIQueue(); -+ } else { -+ queueMicrotask(flushUIQueue); - } - } - } -+let offset = 0; - function flushUIQueue() { -- queueMicrotask(() => { -- const queue = runOnUIQueue; -- runOnUIQueue = []; -- requestAnimationFrameImpl(() => { -- queue.forEach(([workletFunction, workletArgs, jobResolve]) => { -- const result = workletFunction(...workletArgs); -- if (jobResolve) { -- jobResolve(result); -+ const queue = runOnUIQueue; -+ runOnUIQueue = []; -+ requestAnimationFrame(() => { -+ offset = 0; -+ while (queue.length > offset) { -+ try { -+ drainUIQueue(queue); -+ } catch (e) { -+ const [, , , jobReject] = queue[offset - 1]; -+ if (jobReject) { -+ jobReject(e); -+ } else { -+ console.error(e); - } -- }); -- }); -+ } -+ } - }); - } --const requestAnimationFrameImpl = !globalThis.requestAnimationFrame ? mockedRequestAnimationFrame : globalThis.requestAnimationFrame; --//# sourceMappingURL=threads.js.map -\ No newline at end of file -+function drainUIQueue(queue) { -+ while (queue.length > offset) { -+ const [workletFunction, workletArgs, jobResolve] = queue[offset]; -+ offset++; -+ const result = workletFunction(...workletArgs); -+ if (jobResolve) { -+ jobResolve(result); -+ } -+ } -+} -+//# sourceMappingURL=threads.js.map diff --git a/patches/react-native-worklets@0.11.3.patch.md b/patches/react-native-worklets@0.11.3.patch.md deleted file mode 100644 index fb9e2feaeb..0000000000 --- a/patches/react-native-worklets@0.11.3.patch.md +++ /dev/null @@ -1,37 +0,0 @@ -# react-native-worklets@0.11.3.patch - -Backport of https://github.com/software-mansion/react-native-reanimated/pull/10167 -("fix(Worklets): web scheduleOnUI implementation on errors"). - -## The bug - -On web, `scheduleOnUI`/`runOnUI` batch their callbacks per animation frame and -run them with `queue.forEach(...)`. If any callback in the batch throws, -`forEach` aborts immediately and every callback still queued after it is -silently dropped - it never runs, and any `runOnUIAsync` promise for it never -resolves or rejects. - -Reanimated's own internals rely on those callbacks running in order (e.g. to -populate `frameCallbackRegistry`), so a single throwing worklet in a batch can -leave later, unrelated frame callbacks referencing state that was never set -up, surfacing as: - -``` -TypeError: can't access property "startTime", this.frameCallbackRegistry.get(...) is undefined -``` - -## The fix - -Replace the `forEach` batch runner with a `while` loop (`drainUIQueue`) that -tracks its position via an `offset`, wrapped in a `try`/`catch`. A throw now -only aborts the *current* callback: the loop resumes at the next queued job -instead of abandoning the rest of the batch. Errors are routed to the -matching `runOnUIAsync` promise's `reject` (a new second argument threaded -through `enqueueUI`) if there is one, or `console.error`-ed otherwise, rather -than crashing the whole frame. - -Only `lib/module/threads.js` (the compiled web entry point actually loaded by -the app's webpack build) is patched - `src/threads.ts` is unused here since -this repo's web build resolves the package's `module` field, and native -platforms use the separate `threads.native.ts` implementation untouched by -this PR. diff --git a/patches/react-native@0.86.0.patch b/patches/react-native@0.86.0.patch index 26a40a5bfa..b603f3fefe 100644 --- a/patches/react-native@0.86.0.patch +++ b/patches/react-native@0.86.0.patch @@ -23,180 +23,6 @@ index 1b02e8b2d39672063551411d5c403a69b671a869..b3481c1b98b45dea769035140dc2fd8d - (void)setFrame:(CGRect)frame { [super setFrame:frame]; -diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm -index a087536f3af0d33b13fe38d8abd1bc6d7935def2..01f5c884ea4772350c0ebe6263723d97632f2b74 100644 ---- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm -+++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm -@@ -396,7 +396,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & - - MAP_SCROLL_VIEW_PROP(zoomScale); - -- if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset) { -+ // When disabling centerContent, reset inset to prop value -+ // (enabling is handled automatically by the setCenterContent: setter) -+ if (oldScrollViewProps.centerContent && !newScrollViewProps.centerContent) { -+ _scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset); -+ } -+ -+ // Only apply contentInset from props if centerContent is disabled -+ // When centerContent is enabled, the inset is calculated by centerContentIfNeeded -+ if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset && !newScrollViewProps.centerContent) { - _scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset); - } - -@@ -523,7 +531,7 @@ - (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event - } - } - -- return isPointInside ? self : nil; -+ return isPointInside ? _scrollView : nil; - } - - /* -@@ -1133,6 +1141,11 @@ - (RCTVirtualViewContainerState *)virtualViewContainerState - return _virtualViewContainerState; - } - -++ (BOOL)shouldBeRecycled -+{ -+ return NO; -+} -+ - @end - - Class RCTScrollViewCls(void) -diff --git a/React/Views/RefreshControl/RCTRefreshControl.h b/React/Views/RefreshControl/RCTRefreshControl.h -index ed306d7cadbf36a2fed79be8bd9d68b5dca135bd..d447dad534fefa9fcbdbbde6dcbbdcddadd5a824 100644 ---- a/React/Views/RefreshControl/RCTRefreshControl.h -+++ b/React/Views/RefreshControl/RCTRefreshControl.h -@@ -18,6 +18,7 @@ __attribute__((deprecated("This API will be removed along with the legacy archit - @property (nonatomic, copy) NSString *title; - @property (nonatomic, copy) RCTDirectEventBlock onRefresh; - @property (nonatomic, weak) UIScrollView *scrollView; -+@property (nonatomic, copy) UIColor *customTintColor; - - @end - -diff --git a/React/Views/RefreshControl/RCTRefreshControl.m b/React/Views/RefreshControl/RCTRefreshControl.m -index 2dc86e464264c9450eef18d7b153d35bf6a5cc55..6661dc69a04766afa0284d6e83839b219e98cf57 100644 ---- a/React/Views/RefreshControl/RCTRefreshControl.m -+++ b/React/Views/RefreshControl/RCTRefreshControl.m -@@ -25,6 +25,7 @@ @implementation RCTRefreshControl { - UIColor *_titleColor; - CGFloat _progressViewOffset; - BOOL _hasMovedToWindow; -+ UIColor *_customTintColor; - } - - - (instancetype)init -@@ -60,6 +61,12 @@ - (void)layoutSubviews - _isInitialRender = false; - } - -+- (void)didMoveToSuperview -+{ -+ [super didMoveToSuperview]; -+ [self setTintColor:_customTintColor]; -+} -+ - - (void)didMoveToWindow - { - [super didMoveToWindow]; -@@ -225,6 +232,18 @@ - (void)refreshControlValueChanged - } - } - -+// Fix for https://github.com/facebook/react-native/issues/43388 -+// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor -+// is set before the refresh control gets added to the scrollview. We'll call this -+// function whenever the superview changes. We'll also call it if the value of customTintColor -+// changes. -+- (void)setTintColor:(UIColor *)tintColor -+{ -+ if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) { -+ [super setTintColor:tintColor]; -+ } -+} -+ - @end - - #endif // RCT_REMOVE_LEGACY_ARCH -diff --git a/React/Views/RefreshControl/RCTRefreshControlManager.m b/React/Views/RefreshControl/RCTRefreshControlManager.m -index 1e9ff527f4e6691d716da624031113a397876981..44329c5422c6f24d8a437fa35c6f2bad6bf8622b 100644 ---- a/React/Views/RefreshControl/RCTRefreshControlManager.m -+++ b/React/Views/RefreshControl/RCTRefreshControlManager.m -@@ -24,11 +24,12 @@ - (UIView *)view - - RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock) - RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL) --RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) - RCT_EXPORT_VIEW_PROPERTY(title, NSString) - RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor) - RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat) - -+RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor) -+ - RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing) - { - [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { -diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt -index 59775241c80bec99ad3ec080f2425aacc8900c24..426de3aa77cda2032d7b0991e2ca3f8482a438d3 100644 ---- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt -+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt -@@ -459,6 +459,13 @@ public open class ReactViewGroup public constructor(context: Context?) : - inSubviewClippingLoop = true - var clippedSoFar = 0 - for (i in 0.. RCTScrollViewCls(void) +diff --git a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +index b033b7c71914d287470b7b86bd6bf39d311294ba..7e10dc929147fa4474ca9f955f5cd85d27aea935 100644 +--- a/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm ++++ b/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +@@ -827,9 +827,17 @@ static void RCTAddContourEffectToLayer( + } else { + CGSize imageSize = image.size; + UIEdgeInsets imageCapInsets = image.capInsets; ++ // The stretchable middle is whatever lies between the cap insets. The image ++ // may be larger than capInsets + 1 (its size is ceil'd to whole points), so ++ // deriving the middle from the caps rather than assuming a 1pt band keeps ++ // the bottom/right caps at their true size. A phantom cap here makes the ++ // caps overflow sub-pixel-sized layers (e.g. hairline borders), and the ++ // squeezed mesh + nearest-neighbor filtering drops the stroke entirely. + CGRect contentsCenter = CGRect{ + CGPoint{imageCapInsets.left / imageSize.width, imageCapInsets.top / imageSize.height}, +- CGSize{(CGFloat)1.0 / imageSize.width, (CGFloat)1.0 / imageSize.height}}; ++ CGSize{ ++ (imageSize.width - imageCapInsets.left - imageCapInsets.right) / imageSize.width, ++ (imageSize.height - imageCapInsets.top - imageCapInsets.bottom) / imageSize.height}}; + layer.contents = (id)image.CGImage; + layer.contentsScale = image.scale; + +diff --git a/React/Views/RefreshControl/RCTRefreshControl.h b/React/Views/RefreshControl/RCTRefreshControl.h +index ed306d7cadbf36a2fed79be8bd9d68b5dca135bd..d447dad534fefa9fcbdbbde6dcbbdcddadd5a824 100644 +--- a/React/Views/RefreshControl/RCTRefreshControl.h ++++ b/React/Views/RefreshControl/RCTRefreshControl.h +@@ -18,6 +18,7 @@ __attribute__((deprecated("This API will be removed along with the legacy archit + @property (nonatomic, copy) NSString *title; + @property (nonatomic, copy) RCTDirectEventBlock onRefresh; + @property (nonatomic, weak) UIScrollView *scrollView; ++@property (nonatomic, copy) UIColor *customTintColor; + + @end + +diff --git a/React/Views/RefreshControl/RCTRefreshControl.m b/React/Views/RefreshControl/RCTRefreshControl.m +index 2dc86e464264c9450eef18d7b153d35bf6a5cc55..6661dc69a04766afa0284d6e83839b219e98cf57 100644 +--- a/React/Views/RefreshControl/RCTRefreshControl.m ++++ b/React/Views/RefreshControl/RCTRefreshControl.m +@@ -25,6 +25,7 @@ @implementation RCTRefreshControl { + UIColor *_titleColor; + CGFloat _progressViewOffset; + BOOL _hasMovedToWindow; ++ UIColor *_customTintColor; + } + + - (instancetype)init +@@ -60,6 +61,12 @@ - (void)layoutSubviews + _isInitialRender = false; + } + ++- (void)didMoveToSuperview ++{ ++ [super didMoveToSuperview]; ++ [self setTintColor:_customTintColor]; ++} ++ + - (void)didMoveToWindow + { + [super didMoveToWindow]; +@@ -225,6 +232,18 @@ - (void)refreshControlValueChanged + } + } + ++// Fix for https://github.com/facebook/react-native/issues/43388 ++// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor ++// is set before the refresh control gets added to the scrollview. We'll call this ++// function whenever the superview changes. We'll also call it if the value of customTintColor ++// changes. ++- (void)setTintColor:(UIColor *)tintColor ++{ ++ if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) { ++ [super setTintColor:tintColor]; ++ } ++} ++ + @end + + #endif // RCT_REMOVE_LEGACY_ARCH +diff --git a/React/Views/RefreshControl/RCTRefreshControlManager.m b/React/Views/RefreshControl/RCTRefreshControlManager.m +index 1e9ff527f4e6691d716da624031113a397876981..44329c5422c6f24d8a437fa35c6f2bad6bf8622b 100644 +--- a/React/Views/RefreshControl/RCTRefreshControlManager.m ++++ b/React/Views/RefreshControl/RCTRefreshControlManager.m +@@ -24,11 +24,12 @@ - (UIView *)view + + RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL) +-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) + RCT_EXPORT_VIEW_PROPERTY(title, NSString) + RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor) + RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat) + ++RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor) ++ + RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing) + { + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { +diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +index 59775241c80bec99ad3ec080f2425aacc8900c24..426de3aa77cda2032d7b0991e2ca3f8482a438d3 100644 +--- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt ++++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +@@ -459,6 +459,13 @@ public open class ReactViewGroup public constructor(context: Context?) : + inSubviewClippingLoop = true + var clippedSoFar = 0 + for (i in 0.. UIManager::findShadowNodeByTag_DEPRECATED( + auto shadowNode = std::shared_ptr{}; + + shadowTreeRegistry_.enumerate([&](const ShadowTree& shadowTree, bool& stop) { +- // Obtain a pointer to the root node. The flag-gated path uses +- // getCurrentRevision() which keeps the root alive via shared_ptr for +- // the entire traversal, fixing a use-after-free race condition. +- RootShadowNode::Shared rootShadowNodeHolder; +- const RootShadowNode* rootShadowNode = nullptr; +- if (ReactNativeFeatureFlags::fixFindShadowNodeByTagRaceCondition()) { +- rootShadowNodeHolder = shadowTree.getCurrentRevision().rootShadowNode; +- rootShadowNode = rootShadowNodeHolder.get(); +- } else { +- // TODO(T257154369): Remove after flag rollout. +- // The public interface of `ShadowTree` discourages accessing a stored +- // pointer to a root node because of the possible data race. +- // To work around this, we ask for a commit and immediately cancel it +- // returning `nullptr` instead of a new shadow tree. +- // We don't want to add a way to access a stored pointer to a root +- // node because this `findShadowNodeByTag` is deprecated. It is only +- // added to make migration to the new architecture easier. +- shadowTree.tryCommit( +- [&](const RootShadowNode& oldRootShadowNode) { +- rootShadowNode = &oldRootShadowNode; +- return nullptr; +- }, +- {/* default commit options */}); +- } ++ auto rootShadowNodeHolder = shadowTree.getCurrentRevision().rootShadowNode; ++ const auto* rootShadowNode = rootShadowNodeHolder.get(); + + if (rootShadowNode != nullptr) { + const auto& children = rootShadowNode->getChildren(); diff --git a/patches/react-native@0.86.0.patch.md b/patches/react-native@0.86.0.patch.md index e024b0cdcf..ec9833faaa 100644 --- a/patches/react-native@0.86.0.patch.md +++ b/patches/react-native@0.86.0.patch.md @@ -1,5 +1,36 @@ # ***This second part of this patch is load bearing, do not remove.*** +## Scheduler delegate invalidation - iOS use-after-free + +Fixes Sentry issue APP-T28X: an `EXC_BAD_ACCESS` in +`Scheduler::uiManagerDidFinishTransaction` or +`Scheduler::uiManagerDidDispatchCommand` after a queued rendering update +outlives its captured raw `SchedulerDelegate` pointer. + +React Native 0.86 contains the invalidation-token guard from +facebook/react-native#56680, but `enableSchedulerDelegateInvalidation` is false +for the stable release level used by Expo. Override only this flag in +`ReactNativeFeatureFlagsOverridesOSSStable` instead of opting the app into all +experimental React Native flags. + +**TODO: Remove after upgrading to a React Native release that closes the +queued Scheduler delegate lifetime race by default.** + +## UIManager.cpp Patch - Fabric focus navigation use-after-free + +Fixes Sentry issue APP-T4H9: a SIGSEGV in +`FabricUIManagerBinding::findNextFocusableElement` during focus navigation. + +React Native 0.86 contains the safe implementation behind +`fixFindShadowNodeByTagRaceCondition`, but the public default is false. The +fallback captures a raw root shadow-node pointer in `tryCommit` and dereferences +it after the lock is released, allowing a concurrent commit or surface stop to +free the node first. This backports the final upstream implementation, which +holds the current revision's `shared_ptr` for the entire traversal. + +**TODO: Remove after bumping React Native to a release containing +facebook/react-native#56850.** + ## RefreshControl Patch - iOS 17.4 Haptic Regression Patching `RCTRefreshControl.mm` temporarily to play an impact haptic on refresh when using iOS 17.4 or higher. Since @@ -144,3 +175,22 @@ PR: https://github.com/facebook/react-native/pull/57483 Issue: https://github.com/react/react-native/issues/53450#issuecomment-3298157830 Bandaid fix taken from: https://github.com/react/react-native/commit/581d643a9e59fd88f93757f80194e1efd11bd0e5 + +## RCTViewComponentView.mm Patch - Hairline border strokes dropped at certain subpixel Y offsets on New Arch + +Symptom: dividers built as `borderTopWidth: hairlineWidth` vanish on some screens and not +others, deterministically by the view's absolute subpixel Y. A background fill of the same +geometry always renders. + +Cause: Fabric draws borders as a stretched 9-slice image. The consumer +(`RCTAddContourEffectToLayer`) hardcodes the stretchable middle as a 1pt band, which matched +the image the producer built until RN 0.81. facebook/react-native#54237 changed the image +size to `ceil(insets) + 1 + ceil(insets)` without updating that formula, so for fractional +(hairline) insets the labels no longer match the image: transparent filler is treated as a +rigid cap, and when squeezed into a one-pixel-tall layer the sampling can land on it instead +of the stroke - no line. + +Fix: compute the middle from the cap insets (`size - caps`) instead of assuming 1pt. + +Upstream issue: https://github.com/react/react-native/issues/58054 (repro: +https://github.com/abulenok/HairlineBorderRepro, fails identically on 0.86.0 and 0.87.0). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3815e3f2e5..2889ae4823 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,9 +208,8 @@ overrides: '@react-native/normalize-colors': 0.86.0 '@expo/image-utils': 0.8.12 '@types/estree': 1.0.6 - react-native-compressor: 1.13.0 - react-native-reanimated: 4.5.3 - react-native-worklets: 0.11.3 + react-native-reanimated: 4.6.0 + react-native-worklets: 0.12.1 psl: 1.9.0 '@types/psl': 1.1.1 react-native-screens: 4.26.2 @@ -218,25 +217,23 @@ overrides: packageExtensionsChecksum: sha256-KbeCj471Qk3TA758q8nLPkiGCPDA0TX18iO/Coy89lo= patchedDependencies: + '@formatjs/bigdecimal@0.2.5': 53afe18a845c1469b8b9002a460582559dfd52f502bc66a0f4c05662c2619291 '@sentry/expo-upload-sourcemaps@8.18.0': 2368e1e1986165e7ead3c60017b78b2f9ade1b6a6b2f5ad27cd428608c6f12ae expo-age-range@57.0.2: c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306 expo-haptics@57.0.1: b33910ba2753c4eccdc885b449e6e33aa90b9cefa75ca8639762a26013141410 expo-media-library@57.0.3: 2b84c17999bb0c977eaef7fa8ac297f7074d91c94fb63726b42d2a277b26b39a - expo-modules-core@57.0.8: 86e57bb33bc6c3f7021e948e099c2b5378772147b92a82a16e2e90ddcb857ace + expo-modules-core@57.0.8: 7c2fd5f0b146ee5208ea607ccdd3127af95bf7202e733cd92c9ca5d2350b4832 expo-notifications@57.0.7: 542d7d2024b364ba601b7f2af041353dccde555db0e6ecf0b5948c90e6b33a75 expo-updates@57.0.10: 04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487 expo@57.0.8: 1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a - react-native-compressor@1.13.0: 58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925 react-native-date-picker@5.0.13: 92943fb79d17d7342a29bbb12b0d8ee3cf6f7bca12ed322dc8d124a3e9fb75bd react-native-dotenv@3.4.11: 16b34eb974399935d3cf7c2526534f906991ccd876215176658347059cdd2021 react-native-drawer-layout@4.2.3: 74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130 react-native-keyboard-controller@1.21.9: ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271 react-native-pager-view@6.8.0: c8316acd33c9aef6531e8c272c2bfab7bd02af1255969eeaab2bad5ab48531cc - react-native-reanimated@4.5.3: cb7a94c574c89fae48a259ea1ba4c4c97b1db634237336237805491b7c1234de react-native-screens@4.26.2: ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c react-native-svg@15.15.4: ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310 - react-native-worklets@0.11.3: 8ebbcf528fc731459ac4a4eff612a50dfb7e24462a9df201e4aaa81a201343f0 - react-native@0.86.0: dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909 + react-native@0.86.0: 239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06 importers: @@ -256,46 +253,46 @@ importers: version: 0.7.5 '@bitdrift/react-native': specifier: ^0.6.8 - version: 0.6.14(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + version: 0.6.14(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) '@braintree/sanitize-url': specifier: ^6.0.2 version: 6.0.4 '@bsky.app/alf': specifier: ^0.1.15 - version: 0.1.15(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.1.15(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky.app/expo-dynamic-app-icon': specifier: ^1.8.5 - version: 1.8.5(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 1.8.5(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky.app/expo-guess-language': specifier: ^0.2.8 - version: 0.2.8(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.2.8(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky.app/expo-image-crop-tool': specifier: ^0.5.1 - version: 0.5.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.5.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky.app/expo-scroll-edge-effect': specifier: ^0.1.9 - version: 0.1.9(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.1.9(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky.app/expo-translate-text': specifier: ^0.2.9 - version: 0.2.9(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.2.9(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky.app/peek-menu': specifier: ^0.3.2 - version: 0.3.2(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.3.2(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky.app/react-native-uitextview': - specifier: ^2.7.0 - version: 2.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + specifier: ^2.7.1 + version: 2.7.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky.app/sift': specifier: ^0.3.9 - version: 0.3.9(expo@57.0.8)(react-native-safe-area-context@5.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.3.9(expo@57.0.8)(react-native-safe-area-context@5.7.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky.app/tapper': specifier: ^0.6.1 - version: 0.6.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(tlds@1.261.0) + version: 0.6.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(tlds@1.261.0) '@bsky.app/video': specifier: 0.3.6 - version: 0.3.6(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.3.6(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky.app/video-compressor': specifier: 0.2.0 - version: 0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@bsky/sdk': specifier: 1.1.2 version: 1.1.2(@atproto/lex@0.3.8) @@ -346,19 +343,19 @@ importers: version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(react@19.2.3) '@react-native-async-storage/async-storage': specifier: 2.2.0 - version: 2.2.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + version: 2.2.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) '@react-navigation/bottom-tabs': specifier: ^7.15.5 - version: 7.16.0(69582b62b99c5a25dfa1fbf4667c7e56) + version: 7.16.0(e010b3b7e876375fdd4841f6f289d0a2) '@react-navigation/native': specifier: ^7.1.33 - version: 7.2.4(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 7.2.4(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@react-navigation/native-stack': specifier: ^7.14.4 - version: 7.15.0(69582b62b99c5a25dfa1fbf4667c7e56) + version: 7.15.0(e010b3b7e876375fdd4841f6f289d0a2) '@sentry/react-native': specifier: ~8.18.0 - version: 8.18.0(@expo/env@2.4.2(supports-color@8.1.1))(dotenv@16.6.1)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 8.18.0(@expo/env@2.4.2(supports-color@8.1.1))(dotenv@16.6.1)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@tanstack/query-async-storage-persister': specifier: ^5.96.2 version: 5.100.10 @@ -422,6 +419,9 @@ importers: bcp-47-match: specifier: ^2.0.3 version: 2.0.3 + bidi-js: + specifier: ^1.0.3 + version: 1.0.3 date-fns: specifier: ^4.4.0 version: 4.4.0 @@ -439,52 +439,52 @@ importers: version: 5.0.4 expo: specifier: 57.0.8 - version: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + version: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-age-range: specifier: 57.0.2 - version: 57.0.2(patch_hash=c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + version: 57.0.2(patch_hash=c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) expo-application: specifier: ~57.0.2 version: 57.0.2(expo@57.0.8) expo-asset: specifier: ~57.0.7 - version: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + version: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) expo-blur: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-build-properties: specifier: ~57.0.7 version: 57.0.12(expo@57.0.8) expo-camera: specifier: ~57.0.3 - version: 57.0.3(@types/emscripten@1.41.5)(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 57.0.3(@types/emscripten@1.41.5)(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-clipboard: specifier: ~57.0.1 - version: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-contacts: specifier: ^57.0.2 - version: 57.0.4(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 57.0.4(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-dev-client: specifier: ~57.0.9 - version: 57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + version: 57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) expo-device: specifier: ~57.0.1 version: 57.0.1(expo@57.0.8) expo-file-system: specifier: ~57.0.1 - version: 57.0.4(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + version: 57.0.4(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) expo-font: specifier: ~57.0.1 - version: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-glass-effect: specifier: 57.0.1 - version: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-haptics: specifier: ~57.0.1 version: 57.0.1(patch_hash=b33910ba2753c4eccdc885b449e6e33aa90b9cefa75ca8639762a26013141410)(expo@57.0.8) expo-image: specifier: 57.0.1 - version: 57.0.1(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 57.0.1(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-image-manipulator: specifier: ~57.0.6 version: 57.0.10(expo@57.0.8) @@ -499,10 +499,10 @@ importers: version: 57.0.1(expo@57.0.8)(react@19.2.3) expo-linear-gradient: specifier: ~57.0.1 - version: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-linking: specifier: ~57.0.4 - version: 57.0.6(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + version: 57.0.6(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) expo-localization: specifier: ~57.0.1 version: 57.0.1(expo@57.0.8)(react@19.2.3) @@ -511,25 +511,25 @@ importers: version: 57.0.11(expo@57.0.8) expo-media-library: specifier: 57.0.3 - version: 57.0.3(patch_hash=2b84c17999bb0c977eaef7fa8ac297f7074d91c94fb63726b42d2a277b26b39a)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + version: 57.0.3(patch_hash=2b84c17999bb0c977eaef7fa8ac297f7074d91c94fb63726b42d2a277b26b39a)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) expo-modules-core: specifier: 57.0.8 - version: 57.0.8(patch_hash=86e57bb33bc6c3f7021e948e099c2b5378772147b92a82a16e2e90ddcb857ace)(c6ee2c3257efee1775b3ba0a37d05090) + version: 57.0.8(patch_hash=7c2fd5f0b146ee5208ea607ccdd3127af95bf7202e733cd92c9ca5d2350b4832)(92d1a74bc123f334ae9968fa1c80814f) expo-notifications: specifier: 57.0.7 - version: 57.0.7(patch_hash=542d7d2024b364ba601b7f2af041353dccde555db0e6ecf0b5948c90e6b33a75)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + version: 57.0.7(patch_hash=542d7d2024b364ba601b7f2af041353dccde555db0e6ecf0b5948c90e6b33a75)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) expo-paste-input: specifier: ^0.2.1 - version: 0.2.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.2.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-privacy-sensitive: specifier: ^0.2.0 - version: 0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-screen-orientation: specifier: ~57.0.1 - version: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + version: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) expo-sharing: specifier: ~57.0.7 - version: 57.0.13(@typescript/typescript6@6.0.2)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + version: 57.0.13(@typescript/typescript6@6.0.2)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) expo-sms: specifier: ^57.0.1 version: 57.0.1(expo@57.0.8) @@ -538,19 +538,19 @@ importers: version: 57.0.7(@typescript/typescript6@6.0.2)(expo@57.0.8)(supports-color@8.1.1) expo-system-ui: specifier: ~57.0.1 - version: 57.0.2(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) + version: 57.0.2(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) expo-updates: specifier: 57.0.10 - version: 57.0.10(patch_hash=04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487)(expo-dev-client@57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)))(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + version: 57.0.10(patch_hash=04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487)(expo-dev-client@57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)))(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) expo-video: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-video-thumbnails: specifier: ^57.0.1 version: 57.0.1(expo@57.0.8) expo-web-browser: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + version: 57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 @@ -631,61 +631,58 @@ importers: version: 5.1.2(react-is@19.2.6)(react@19.2.3) react-native: specifier: 0.86.0 - version: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-compressor: - specifier: 1.13.0 - version: 1.13.0(patch_hash=58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) react-native-date-picker: specifier: ^5.0.13 - version: 5.0.13(patch_hash=92943fb79d17d7342a29bbb12b0d8ee3cf6f7bca12ed322dc8d124a3e9fb75bd)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 5.0.13(patch_hash=92943fb79d17d7342a29bbb12b0d8ee3cf6f7bca12ed322dc8d124a3e9fb75bd)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-device-attest: specifier: ^0.1.6 - version: 0.1.6(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.1.6(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-drawer-layout: specifier: ^4.2.3 - version: 4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(ac885d87795f1c83dff851cd6dfdabcc) + version: 4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(react-native-gesture-handler@2.32.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native-reanimated@4.6.0(92d1a74bc123f334ae9968fa1c80814f))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-edge-to-edge: specifier: ^1.8.1 - version: 1.8.1(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 1.8.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-gesture-handler: specifier: ~2.32.0 - version: 2.32.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 2.32.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-keyboard-controller: specifier: 1.21.9 - version: 1.21.9(patch_hash=ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271)(react-native-reanimated@4.5.3(patch_hash=cb7a94c574c89fae48a259ea1ba4c4c97b1db634237336237805491b7c1234de)(c6ee2c3257efee1775b3ba0a37d05090))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 1.21.9(patch_hash=ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271)(react-native-reanimated@4.6.0(92d1a74bc123f334ae9968fa1c80814f))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-mmkv: specifier: ^3.3.3 - version: 3.3.3(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 3.3.3(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-pager-view: specifier: 6.8.0 - version: 6.8.0(patch_hash=c8316acd33c9aef6531e8c272c2bfab7bd02af1255969eeaab2bad5ab48531cc)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 6.8.0(patch_hash=c8316acd33c9aef6531e8c272c2bfab7bd02af1255969eeaab2bad5ab48531cc)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-progress: specifier: ^5.0.1 - version: 5.0.1(react-native-svg@15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)) + version: 5.0.1(react-native-svg@15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)) react-native-qrcode-styled: specifier: ^0.3.3 - version: 0.3.3(react-native-svg@15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 0.3.3(react-native-svg@15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-reanimated: - specifier: 4.5.3 - version: 4.5.3(patch_hash=cb7a94c574c89fae48a259ea1ba4c4c97b1db634237336237805491b7c1234de)(c6ee2c3257efee1775b3ba0a37d05090) + specifier: 4.6.0 + version: 4.6.0(92d1a74bc123f334ae9968fa1c80814f) react-native-safe-area-context: specifier: ~5.7.0 - version: 5.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 5.7.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-screens: specifier: 4.26.2 - version: 4.26.2(patch_hash=ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 4.26.2(patch_hash=ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-scroll-forwarder: specifier: link:./modules/react-native-scroll-forwarder version: link:modules/react-native-scroll-forwarder react-native-svg: specifier: 15.15.4 - version: 15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-uuid: specifier: ^2.0.3 version: 2.0.4 react-native-view-shot: specifier: ^5.1.0 - version: 5.1.1(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 5.1.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-web: specifier: ^0.21.0 version: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -694,10 +691,10 @@ importers: version: 1.0.2(file-loader@6.2.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) react-native-webview: specifier: ^13.16.1 - version: 13.17.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + version: 13.17.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-worklets: - specifier: 0.11.3 - version: 0.11.3(patch_hash=8ebbcf528fc731459ac4a4eff612a50dfb7e24462a9df201e4aaa81a201343f0)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + specifier: 0.12.1 + version: 0.12.1(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) react-remove-scroll-bar: specifier: ^2.3.8 version: 2.3.8(@types/react@19.2.18)(react@19.2.3) @@ -718,7 +715,7 @@ importers: version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) sonner-native: specifier: 0.26.4 - version: 0.26.4(669e6adb8ceb9d91fb46b2f510aae203) + version: 0.26.4(a1115538eb9b7f01f00730be61406201) tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -770,7 +767,7 @@ importers: version: 3.6.1(supports-color@8.1.1)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) '@testing-library/react-native': specifier: ^13.2.0 - version: 13.3.3(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react-test-renderer@19.2.3(react@19.2.3))(react@19.2.3) + version: 13.3.3(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react-test-renderer@19.2.3(react@19.2.3))(react@19.2.3) '@types/jest': specifier: 29.5.14 version: 29.5.14 @@ -833,7 +830,7 @@ importers: version: 29.7.0(@types/node@24.12.4)(supports-color@8.1.1) jest-expo: specifier: ~57.0.2 - version: 57.0.4(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(expo@57.0.8)(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + version: 57.0.4(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(expo@57.0.8)(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) jest-junit: specifier: ^16.0.0 version: 16.0.0 @@ -858,6 +855,9 @@ importers: react-refresh: specifier: ^0.14.0 version: 0.14.2 + sharp: + specifier: ^0.35.4 + version: 0.35.4(@types/node@24.12.4) svgo: specifier: ^4.0.2 version: 4.0.2 @@ -1714,8 +1714,8 @@ packages: react: '*' react-native: '*' - '@bsky.app/react-native-uitextview@2.7.0': - resolution: {integrity: sha512-Z++CL+y01bHUTH0L7YfSB1QYr0Qjk7/2XUTp8NAfxnw3QnlWkClZNV7n1YOJ51tOAAhtFaVlvFDlLILDYqo2GQ==} + '@bsky.app/react-native-uitextview@2.7.1': + resolution: {integrity: sha512-hy1UCDFwBJc+GAzezaLx5Xl++lqRoN4/Y38VbUQ27htYfGXSSUl9UAc1hmaiFxKwRD4KZjclo6JQuB5cRWbVPg==} peerDependencies: react: '*' react-native: '>=0.81.5' @@ -1768,6 +1768,9 @@ packages: resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} engines: {node: '>=0.8.0'} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@emoji-mart/data@1.2.1': resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} @@ -2200,6 +2203,168 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@ipld/dag-cbor@9.2.7': resolution: {integrity: sha512-ZmfXmElRWATr+hoUTSAOr6HUcjVhOcNHDqgczc76qte2DHHFEK0ZhNzUcdTDQhF/VSIvf2ioaRTRLWwLc83sNw==} @@ -4527,6 +4692,9 @@ packages: bcp-47@2.1.0: resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} @@ -5649,7 +5817,7 @@ packages: peerDependencies: react: '*' react-native: '*' - react-native-worklets: 0.11.3 + react-native-worklets: 0.12.1 peerDependenciesMeta: react-native-worklets: optional: true @@ -8169,13 +8337,6 @@ packages: react: '>=18.0.0' react-is: '>=18.0.0' - react-native-compressor@1.13.0: - resolution: {integrity: sha512-vJYbrcjz2S7kgE3Q7444F71CjEDq5Qg6sGn67N9fJ0I1K6EhB/ZxpULdDe3FxJ4h/ncJc0oxOzNWZhxtGTqcQw==} - engines: {node: '>= 16.0.0'} - peerDependencies: - react: '*' - react-native: '*' - react-native-date-picker@5.0.13: resolution: {integrity: sha512-qCLUODZVsJetO5zuoXjw1D39K527XWqBG8sOfhWdHyPzf13h8RXR1/RSKd1N0fdRDi5GdyizYmB0lPAK12/hbw==} peerDependencies: @@ -8200,7 +8361,7 @@ packages: react: '>= 18.2.0' react-native: '*' react-native-gesture-handler: '>= 2.0.0' - react-native-reanimated: 4.5.3 + react-native-reanimated: 4.6.0 react-native-edge-to-edge@1.8.1: resolution: {integrity: sha512-bhvsKqeX9PGkY9wBUk9vni/tJNJdKtLPbs/j3e/3CdV4JmUWfTXYYoL+4Hc8Wmej+5eJxkc8KOFa454ruFWBCA==} @@ -8225,7 +8386,7 @@ packages: peerDependencies: react: '*' react-native: '*' - react-native-reanimated: 4.5.3 + react-native-reanimated: 4.6.0 react-native-mmkv@3.3.3: resolution: {integrity: sha512-GMsfOmNzx0p5+CtrCFRVtpOOMYNJXuksBVARSQrCFaZwjUyHJdQzcN900GGaFFNTxw2fs8s5Xje//RDKj9+PZA==} @@ -8252,12 +8413,12 @@ packages: react-native: '*' react-native-svg: '*' - react-native-reanimated@4.5.3: - resolution: {integrity: sha512-+owIckpD4sA13XKHaLrr8V1RP4paqoxKvz/jy2wl/ULjMsfcRlfIJdKJxR1kcQr4McBZ6hAoPo01NyLnmy2ycQ==} + react-native-reanimated@4.6.0: + resolution: {integrity: sha512-9vbQmok5BPX2J3RLPFt2UnwmK2QQoMDPDi0VB4a9JWIiEc55pdzvwEsdH91V/JXKNErqM6a9fElTBMc/1cbtBA==} peerDependencies: react: '*' - react-native: 0.83 - 0.86 - react-native-worklets: 0.11.3 + react-native: 0.83 - 0.87 + react-native-worklets: 0.12.1 react-native-safe-area-context@5.7.0: resolution: {integrity: sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==} @@ -8306,13 +8467,13 @@ packages: react: '*' react-native: '*' - react-native-worklets@0.11.3: - resolution: {integrity: sha512-paLRlVFTrrGhkGTi37LHwtaJbrTftA54/zAx1h/ONSBw7XejZn6XUUl1KpM6EOMwsbw7TEGuxJXTAp/wkvuMqQ==} + react-native-worklets@0.12.1: + resolution: {integrity: sha512-/u7B+Akrox32IQUi6+0OHwWAfBC/A5LVdt000RL8OKB4L1pHTvIDwVyMP6Nw7ybuuoQYS8PLlpmxTE1Ce7TqWg==} peerDependencies: '@babel/core': '*' '@react-native/metro-config': '*' react: '*' - react-native: 0.83 - 0.86 + react-native: 0.83 - 0.87 react-native@0.86.0: resolution: {integrity: sha512-17ALh/dd6AO4pgOVmOO5Axll5PbErEo3XFyLokyzW6usyi+OShIEPwUW26wLPlhVifgSOIfECCH0WN+0IqtJ1w==} @@ -8609,6 +8770,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.2: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} @@ -8659,6 +8825,15 @@ packages: shallow-equal@3.1.0: resolution: {integrity: sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -8747,11 +8922,11 @@ packages: react: '*' react-native: '*' react-native-gesture-handler: '>=2.28.0' - react-native-reanimated: 4.5.3 + react-native-reanimated: 4.6.0 react-native-safe-area-context: ^5.6.0 react-native-screens: 4.26.2 react-native-svg: ^15.12.1 - react-native-worklets: 0.11.3 + react-native-worklets: 0.12.1 sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} @@ -10681,94 +10856,94 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@bitdrift/react-native@0.6.14(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)': + '@bitdrift/react-native@0.6.14(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)': dependencies: '@expo/config-plugins': 9.1.7(supports-color@8.1.1) fast-json-stringify: 6.4.0 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) transitivePeerDependencies: - supports-color '@braintree/sanitize-url@6.0.4': {} - '@bsky.app/alf@0.1.15(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/alf@0.1.15(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) react-responsive: 10.0.1(react@19.2.3) - '@bsky.app/expo-dynamic-app-icon@1.8.5(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/expo-dynamic-app-icon@1.8.5(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: '@expo/image-utils': 0.8.12 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) xcode: 3.0.1 - '@bsky.app/expo-guess-language@0.2.8(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/expo-guess-language@0.2.8(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) lande: 1.0.10 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - '@bsky.app/expo-image-crop-tool@0.5.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/expo-image-crop-tool@0.5.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - '@bsky.app/expo-scroll-edge-effect@0.1.9(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/expo-scroll-edge-effect@0.1.9(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: '@types/jest': 30.0.0 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - '@bsky.app/expo-translate-text@0.2.9(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/expo-translate-text@0.2.9(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - '@bsky.app/peek-menu@0.3.2(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/peek-menu@0.3.2(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - '@bsky.app/react-native-uitextview@2.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/react-native-uitextview@2.7.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - '@bsky.app/sift@0.3.9(expo@57.0.8)(react-native-safe-area-context@5.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/sift@0.3.9(expo@57.0.8)(react-native-safe-area-context@5.7.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-safe-area-context: 5.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-safe-area-context: 5.7.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - '@bsky.app/tapper@0.6.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(tlds@1.261.0)': + '@bsky.app/tapper@0.6.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(tlds@1.261.0)': dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) tlds: 1.261.0 - '@bsky.app/video-compressor@0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/video-compressor@0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) mediabunny: 1.49.0 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - '@bsky.app/video@0.3.6(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@bsky.app/video@0.3.6(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) '@bsky/sdk@1.1.2(@atproto/lex@0.3.8)': dependencies: @@ -10793,6 +10968,11 @@ snapshots: dependencies: '@types/hammerjs': 2.0.46 + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@emoji-mart/data@1.2.1': {} '@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@19.2.3)': @@ -10905,7 +11085,7 @@ snapshots: '@eslint/js@8.57.1': optional: true - '@expo/cli@57.0.16(190a2f1debf63fd2383c9ce3f4eda816)': + '@expo/cli@57.0.16(6d22268ca81ff460e3b175e3d8d42676)': dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/config': 57.0.8(@typescript/typescript6@6.0.2)(supports-color@8.1.1) @@ -10915,7 +11095,7 @@ snapshots: '@expo/image-utils': 0.8.12 '@expo/inline-modules': 0.1.6(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@expo/json-file': 11.0.1 - '@expo/log-box': 57.0.3(@expo/dom-webview@57.0.1)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@expo/log-box': 57.0.3(@expo/dom-webview@57.0.1)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@expo/metro': 56.0.0(supports-color@8.1.1) '@expo/metro-config': 57.0.8(@typescript/typescript6@6.0.2)(expo@57.0.8)(supports-color@8.1.1) '@expo/metro-file-map': 57.0.1(supports-color@8.1.1) @@ -10924,7 +11104,7 @@ snapshots: '@expo/plist': 0.8.1 '@expo/prebuild-config': 57.0.12(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@expo/require-utils': 57.0.4(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@expo/router-server': 57.0.6(cb97be171d73e541f78f885e29e29406) + '@expo/router-server': 57.0.6(a2144abeab7261018ab62471bcf210e4) '@expo/schema-utils': 57.0.2 '@expo/spawn-async': 1.8.0 '@expo/ws-tunnel': 2.0.0(ws@8.20.1) @@ -10941,7 +11121,7 @@ snapshots: connect: 3.7.0(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) dnssd-advertise: 1.1.6 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-server: 57.0.3 fetch-nodeshim: 0.4.10 getenv: 2.0.0 @@ -10968,7 +11148,7 @@ snapshots: ws: 8.20.1 zod: 3.25.76 optionalDependencies: - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) transitivePeerDependencies: - '@expo/dom-webview' - '@expo/metro-runtime' @@ -11051,18 +11231,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/devtools@57.0.1(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@expo/devtools@57.0.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: chalk: 4.1.2 optionalDependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - '@expo/dom-webview@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@expo/dom-webview@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) '@expo/env@2.4.2(supports-color@8.1.1)': dependencies: @@ -11127,13 +11307,13 @@ snapshots: - supports-color - typescript - '@expo/log-box@57.0.3(@expo/dom-webview@57.0.1)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@expo/log-box@57.0.3(@expo/dom-webview@57.0.1)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - '@expo/dom-webview': 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@expo/dom-webview': 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) anser: 1.4.10 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) stacktrace-parser: 0.1.11 '@expo/metro-config@57.0.8(@typescript/typescript6@6.0.2)(expo@57.0.8)(supports-color@8.1.1)': @@ -11162,7 +11342,7 @@ snapshots: postcss: 8.5.14 resolve-from: 5.0.0 optionalDependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) transitivePeerDependencies: - bufferutil - supports-color @@ -11252,12 +11432,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/router-server@57.0.6(cb97be171d73e541f78f885e29e29406)': + '@expo/router-server@57.0.6(a2144abeab7261018ab62471bcf210e4)': dependencies: debug: 4.4.3(supports-color@8.1.1) - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - expo-constants: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) - expo-font: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + expo-constants: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) + expo-font: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-server: 57.0.3 react: 19.2.3 optionalDependencies: @@ -11288,7 +11468,7 @@ snapshots: copy-webpack-plugin: 10.2.4(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) css-loader: 6.11.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) css-minimizer-webpack-plugin: 3.4.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.12)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-pwa: 0.0.127(expo@57.0.8) find-up: 5.0.0 find-yarn-workspace-root: 2.0.0 @@ -11357,7 +11537,7 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@formatjs/bigdecimal@0.2.5': {} + '@formatjs/bigdecimal@0.2.5(patch_hash=53afe18a845c1469b8b9002a460582559dfd52f502bc66a0f4c05662c2619291)': {} '@formatjs/fast-memoize@3.1.5': {} @@ -11378,12 +11558,12 @@ snapshots: '@formatjs/intl-numberformat@9.3.10': dependencies: - '@formatjs/bigdecimal': 0.2.5 + '@formatjs/bigdecimal': 0.2.5(patch_hash=53afe18a845c1469b8b9002a460582559dfd52f502bc66a0f4c05662c2619291) '@formatjs/intl-localematcher': 0.8.9 '@formatjs/intl-pluralrules@6.3.9': dependencies: - '@formatjs/bigdecimal': 0.2.5 + '@formatjs/bigdecimal': 0.2.5(patch_hash=53afe18a845c1469b8b9002a460582559dfd52f502bc66a0f4c05662c2619291) '@formatjs/intl-localematcher': 0.8.9 '@formatjs/intl-supportedvaluesof@2.3.7': @@ -11414,6 +11594,112 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': optional: true + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.3 + optional: true + + '@img/sharp-darwin-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.3 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.3': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.3': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + optional: true + + '@img/sharp-linux-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.3 + optional: true + + '@img/sharp-linux-arm@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.3 + optional: true + + '@img/sharp-linux-ppc64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.3 + optional: true + + '@img/sharp-linux-riscv64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.3 + optional: true + + '@img/sharp-linux-s390x@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.3 + optional: true + + '@img/sharp-linux-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.3 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + optional: true + + '@img/sharp-wasm32@0.35.4': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 + optional: true + + '@img/sharp-win32-arm64@0.35.4': + optional: true + + '@img/sharp-win32-ia32@0.35.4': + optional: true + + '@img/sharp-win32-x64@0.35.4': + optional: true + '@ipld/dag-cbor@9.2.7': dependencies: cborg: 5.1.1 @@ -12620,10 +12906,10 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@react-native-async-storage/async-storage@2.2.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))': + '@react-native-async-storage/async-storage@2.2.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))': dependencies: merge-options: 3.0.4 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) '@react-native/assets-registry@0.86.0': {} @@ -12816,24 +13102,24 @@ snapshots: '@react-native/typescript-config@0.81.6': {} - '@react-native/virtualized-lists@0.86.0(@types/react@19.2.18)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@react-native/virtualized-lists@0.86.0(@types/react@19.2.18)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) optionalDependencies: '@types/react': 19.2.18 - '@react-navigation/bottom-tabs@7.16.0(69582b62b99c5a25dfa1fbf4667c7e56)': + '@react-navigation/bottom-tabs@7.16.0(e010b3b7e876375fdd4841f6f289d0a2)': dependencies: - '@react-navigation/elements': 2.9.17(caae3333d744f2ce6dff023298657fe1) - '@react-navigation/native': 7.2.4(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@react-navigation/elements': 2.9.17(6eb0e0bcf95273feb037f25c3e10784e) + '@react-navigation/native': 7.2.4(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) color: 4.2.3 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-safe-area-context: 5.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-screens: 4.26.2(patch_hash=ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-safe-area-context: 5.7.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-screens: 4.26.2(patch_hash=ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) sf-symbols-typescript: 2.2.0 transitivePeerDependencies: - '@react-native-masked-view/masked-view' @@ -12850,38 +13136,38 @@ snapshots: use-latest-callback: 0.2.6(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) - '@react-navigation/elements@2.9.17(caae3333d744f2ce6dff023298657fe1)': + '@react-navigation/elements@2.9.17(6eb0e0bcf95273feb037f25c3e10784e)': dependencies: - '@react-navigation/native': 7.2.4(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@react-navigation/native': 7.2.4(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) color: 4.2.3 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-safe-area-context: 5.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-safe-area-context: 5.7.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) use-latest-callback: 0.2.6(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) - '@react-navigation/native-stack@7.15.0(69582b62b99c5a25dfa1fbf4667c7e56)': + '@react-navigation/native-stack@7.15.0(e010b3b7e876375fdd4841f6f289d0a2)': dependencies: - '@react-navigation/elements': 2.9.17(caae3333d744f2ce6dff023298657fe1) - '@react-navigation/native': 7.2.4(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@react-navigation/elements': 2.9.17(6eb0e0bcf95273feb037f25c3e10784e) + '@react-navigation/native': 7.2.4(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) color: 4.2.3 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-safe-area-context: 5.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-screens: 4.26.2(patch_hash=ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-safe-area-context: 5.7.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-screens: 4.26.2(patch_hash=ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) sf-symbols-typescript: 2.2.0 warn-once: 0.1.1 transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@react-navigation/native@7.2.4(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@react-navigation/native@7.2.4(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: '@react-navigation/core': 7.17.4(react@19.2.3) escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.12 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) use-latest-callback: 0.2.6(react@19.2.3) '@react-navigation/routers@7.5.5': @@ -13021,7 +13307,7 @@ snapshots: dependencies: '@sentry/core': 10.64.0 - '@sentry/react-native@8.18.0(@expo/env@2.4.2(supports-color@8.1.1))(dotenv@16.6.1)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': + '@sentry/react-native@8.18.0(@expo/env@2.4.2(supports-color@8.1.1))(dotenv@16.6.1)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: '@sentry/babel-plugin-component-annotate': 5.3.0 '@sentry/browser': 10.64.0 @@ -13030,9 +13316,9 @@ snapshots: '@sentry/expo-upload-sourcemaps': 8.18.0(patch_hash=2368e1e1986165e7ead3c60017b78b2f9ade1b6a6b2f5ad27cd428608c6f12ae)(@expo/env@2.4.2(supports-color@8.1.1))(dotenv@16.6.1) '@sentry/react': 10.64.0(react@19.2.3) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) optionalDependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) transitivePeerDependencies: - '@expo/env' - dotenv @@ -13099,13 +13385,13 @@ snapshots: '@tanstack/query-core': 5.100.10 react: 19.2.3 - '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react-test-renderer@19.2.3(react@19.2.3))(react@19.2.3)': + '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react-test-renderer@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: jest-matcher-utils: 30.4.1 picocolors: 1.1.1 pretty-format: 30.4.1 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) react-test-renderer: 19.2.3(react@19.2.3) redent: 3.0.0 optionalDependencies: @@ -13982,7 +14268,7 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.2 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) transitivePeerDependencies: - '@babel/core' - supports-color @@ -14021,6 +14307,10 @@ snapshots: is-alphanumerical: 2.0.1 is-decimal: 2.0.1 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + big-integer@1.6.52: {} big.js@5.2.2: {} @@ -15075,197 +15365,197 @@ snapshots: jest-mock: 30.4.1 jest-util: 30.4.1 - expo-age-range@57.0.2(patch_hash=c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): + expo-age-range@57.0.2(patch_hash=c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) expo-application@57.0.2(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) - expo-asset@57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): + expo-asset@57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): dependencies: '@expo/image-utils': 0.8.12 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - expo-constants: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + expo-constants: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - expo-blur@57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-blur@57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) expo-build-properties@57.0.12(expo@57.0.8): dependencies: '@expo/schema-utils': 57.0.2 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) resolve-from: 5.0.0 semver: 7.8.0 - expo-camera@57.0.3(@types/emscripten@1.41.5)(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-camera@57.0.3(@types/emscripten@1.41.5)(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: barcode-detector: 3.2.2(@types/emscripten@1.41.5) - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) optionalDependencies: react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) transitivePeerDependencies: - '@types/emscripten' - expo-clipboard@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-clipboard@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-constants@57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1): + expo-constants@57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@expo/env': 2.4.2(supports-color@8.1.1) - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - expo-contacts@57.0.4(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-contacts@57.0.4(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-dev-client@57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): + expo-dev-client@57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - expo-dev-launcher: 57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) - expo-dev-menu: 57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + expo-dev-launcher: 57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + expo-dev-menu: 57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) expo-dev-menu-interface: 57.0.0(expo@57.0.8) expo-manifests: 57.0.1(expo@57.0.8) expo-updates-interface: 57.0.1(expo@57.0.8) transitivePeerDependencies: - react-native - expo-dev-launcher@57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): + expo-dev-launcher@57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: '@expo/schema-utils': 57.0.2 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - expo-dev-menu: 57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + expo-dev-menu: 57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) expo-manifests: 57.0.1(expo@57.0.8) - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) expo-dev-menu-interface@57.0.0(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) - expo-dev-menu@57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): + expo-dev-menu@57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-dev-menu-interface: 57.0.0(expo@57.0.8) - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) expo-device@57.0.1(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) ua-parser-js: 0.7.41 expo-eas-client@57.0.1: {} - expo-file-system@57.0.4(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): + expo-file-system@57.0.4(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-font@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-font@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) fontfaceobserver: 2.3.0 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-glass-effect@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-glass-effect@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) expo-haptics@57.0.1(patch_hash=b33910ba2753c4eccdc885b449e6e33aa90b9cefa75ca8639762a26013141410)(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-image-loader@57.0.1(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-image-manipulator@57.0.10(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-image-loader: 57.0.1(expo@57.0.8) expo-image-picker@57.0.11(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-image-loader: 57.0.1(expo@57.0.8) - expo-image@57.0.1(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-image@57.0.1(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) sf-symbols-typescript: 2.2.0 optionalDependencies: react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) expo-intent-launcher@57.0.1(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-json-utils@57.0.1: {} expo-keep-awake@57.0.1(expo@57.0.8)(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - expo-linear-gradient@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-linear-gradient@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-linking@57.0.6(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): + expo-linking@57.0.6(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): dependencies: - expo-constants: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) + expo-constants: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) invariant: 2.2.4 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) transitivePeerDependencies: - expo - supports-color expo-localization@57.0.1(expo@57.0.8)(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 rtl-detect: 1.1.2 expo-location@57.0.11(expo@57.0.8): dependencies: '@expo/image-utils': 0.8.12 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-manifests@57.0.1(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-json-utils: 57.0.1 - expo-media-library@57.0.3(patch_hash=2b84c17999bb0c977eaef7fa8ac297f7074d91c94fb63726b42d2a277b26b39a)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): + expo-media-library@57.0.3(patch_hash=2b84c17999bb0c977eaef7fa8ac297f7074d91c94fb63726b42d2a277b26b39a)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) expo-modules-autolinking@57.0.10(@typescript/typescript6@6.0.2)(supports-color@8.1.1): dependencies: @@ -15277,81 +15567,81 @@ snapshots: - supports-color - typescript - expo-modules-core@57.0.8(patch_hash=86e57bb33bc6c3f7021e948e099c2b5378772147b92a82a16e2e90ddcb857ace)(c6ee2c3257efee1775b3ba0a37d05090): + expo-modules-core@57.0.8(patch_hash=7c2fd5f0b146ee5208ea607ccdd3127af95bf7202e733cd92c9ca5d2350b4832)(92d1a74bc123f334ae9968fa1c80814f): dependencies: '@expo/expo-modules-macros-plugin': 0.6.1 - expo-modules-jsi: 57.0.4(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + expo-modules-jsi: 57.0.4(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) invariant: 2.2.4 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) optionalDependencies: - react-native-worklets: 0.11.3(patch_hash=8ebbcf528fc731459ac4a4eff612a50dfb7e24462a9df201e4aaa81a201343f0)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + react-native-worklets: 0.12.1(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) - expo-modules-jsi@57.0.4(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): + expo-modules-jsi@57.0.4(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-notifications@57.0.7(patch_hash=542d7d2024b364ba601b7f2af041353dccde555db0e6ecf0b5948c90e6b33a75)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): + expo-notifications@57.0.7(patch_hash=542d7d2024b364ba601b7f2af041353dccde555db0e6ecf0b5948c90e6b33a75)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): dependencies: '@expo/image-utils': 0.8.12 abort-controller: 3.0.0 badgin: 1.2.3 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-application: 57.0.2(expo@57.0.8) - expo-constants: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) + expo-constants: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - expo-paste-input@0.2.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-paste-input@0.2.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-privacy-sensitive@0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-privacy-sensitive@0.2.0(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) expo-pwa@0.0.127(expo@57.0.8): dependencies: '@expo/image-utils': 0.8.12 chalk: 4.1.2 commander: 2.20.0 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) update-check: 1.5.3 - expo-screen-orientation@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): + expo-screen-orientation@57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) expo-server@57.0.3: {} - expo-sharing@57.0.13(@typescript/typescript6@6.0.2)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): + expo-sharing@57.0.13(@typescript/typescript6@6.0.2)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): dependencies: '@expo/config-plugins': 57.0.8(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@expo/config-types': 57.0.2 '@expo/plist': 0.8.1 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - typescript expo-sms@57.0.1(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-splash-screen@57.0.7(@typescript/typescript6@6.0.2)(expo@57.0.8)(supports-color@8.1.1): dependencies: '@expo/config-plugins': 57.0.8(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@expo/image-utils': 0.8.12 - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) xml2js: 0.6.0 transitivePeerDependencies: - supports-color @@ -15359,12 +15649,12 @@ snapshots: expo-structured-headers@57.0.0: {} - expo-system-ui@57.0.2(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1): + expo-system-ui@57.0.2(expo@57.0.8)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@react-native/normalize-colors': 0.86.0 debug: 4.4.3(supports-color@8.1.1) - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) optionalDependencies: react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) transitivePeerDependencies: @@ -15372,9 +15662,9 @@ snapshots: expo-updates-interface@57.0.1(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) - expo-updates@57.0.10(patch_hash=04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487)(expo-dev-client@57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)))(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): + expo-updates@57.0.10(patch_hash=04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487)(expo-dev-client@57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)))(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/plist': 0.8.1 @@ -15382,7 +15672,7 @@ snapshots: arg: 4.1.0 chalk: 4.1.2 debug: 4.4.3(supports-color@8.1.1) - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) expo-eas-client: 57.0.1 expo-manifests: 57.0.1(expo@57.0.8) expo-structured-headers: 57.0.0 @@ -15392,59 +15682,59 @@ snapshots: ignore: 5.3.2 nullthrows: 1.1.1 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) resolve-from: 5.0.0 optionalDependencies: - expo-dev-client: 57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + expo-dev-client: 57.0.13(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) transitivePeerDependencies: - supports-color expo-video-thumbnails@57.0.1(expo@57.0.8): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) - expo-video@57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + expo-video@57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-web-browser@57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): + expo-web-browser@57.0.2(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo@57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a): + expo@57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95): dependencies: '@babel/runtime': 7.29.2 - '@expo/cli': 57.0.16(190a2f1debf63fd2383c9ce3f4eda816) + '@expo/cli': 57.0.16(6d22268ca81ff460e3b175e3d8d42676) '@expo/config': 57.0.8(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@expo/config-plugins': 57.0.8(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@expo/devtools': 57.0.1(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@expo/devtools': 57.0.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@expo/fingerprint': 0.20.8(supports-color@8.1.1) '@expo/local-build-cache-provider': 57.0.6(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@expo/log-box': 57.0.3(@expo/dom-webview@57.0.1)(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@expo/log-box': 57.0.3(@expo/dom-webview@57.0.1)(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@expo/metro': 56.0.0(supports-color@8.1.1) '@expo/metro-config': 57.0.8(@typescript/typescript6@6.0.2)(expo@57.0.8)(supports-color@8.1.1) '@ungap/structured-clone': 1.3.1 babel-preset-expo: 57.0.7(@babel/core@7.29.0(supports-color@8.1.1))(@babel/runtime@7.29.2)(expo@57.0.8)(react-refresh@0.14.2)(supports-color@8.1.1) - expo-asset: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) - expo-constants: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) - expo-file-system: 57.0.4(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) - expo-font: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + expo-asset: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + expo-constants: 57.0.12(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) + expo-file-system: 57.0.4(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + expo-font: 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo-keep-awake: 57.0.1(expo@57.0.8)(react@19.2.3) expo-modules-autolinking: 57.0.10(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - expo-modules-core: 57.0.8(patch_hash=86e57bb33bc6c3f7021e948e099c2b5378772147b92a82a16e2e90ddcb857ace)(c6ee2c3257efee1775b3ba0a37d05090) + expo-modules-core: 57.0.8(patch_hash=7c2fd5f0b146ee5208ea607ccdd3127af95bf7202e733cd92c9ca5d2350b4832)(92d1a74bc123f334ae9968fa1c80814f) pretty-format: 29.7.0 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) react-refresh: 0.14.2 whatwg-url-minimum: 0.1.2 optionalDependencies: - '@expo/dom-webview': 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@expo/dom-webview': 57.0.1(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-dom: 19.2.3(react@19.2.3) react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react-native-webview: 13.17.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-webview: 13.17.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) transitivePeerDependencies: - '@babel/core' - bufferutil @@ -16420,7 +16710,7 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - jest-expo@57.0.4(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(expo@57.0.8)(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): + jest-expo@57.0.4(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(expo@57.0.8)(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): dependencies: '@jest/create-cache-key-function': 29.7.0 '@jest/globals': 29.7.0(supports-color@8.1.1) @@ -16432,12 +16722,12 @@ snapshots: jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1)) json5: 2.2.3 lodash: 4.18.1 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) react-test-renderer: 19.2.3(react@19.2.3) server-only: 0.0.1 stacktrace-js: 2.0.2 optionalDependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) transitivePeerDependencies: - '@babel/core' - bufferutil @@ -18175,120 +18465,115 @@ snapshots: react: 19.2.3 react-is: 19.2.6 - react-native-compressor@1.13.0(patch_hash=58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-date-picker@5.0.13(patch_hash=92943fb79d17d7342a29bbb12b0d8ee3cf6f7bca12ed322dc8d124a3e9fb75bd)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-date-picker@5.0.13(patch_hash=92943fb79d17d7342a29bbb12b0d8ee3cf6f7bca12ed322dc8d124a3e9fb75bd)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-device-attest@0.1.6(expo@57.0.8)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: + expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(7aa1f5f5d2c1af01fea68204d31a3f95) react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - - react-native-device-attest@0.1.6(expo@57.0.8)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): - dependencies: - expo: 57.0.8(patch_hash=1722861d12907a2d412d04230545b2fcbfa5547a79f99d763cb3e8de6f3a6f2a)(b32afa45429c882ad113a248f683620a) - react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) react-native-dotenv@3.4.11(patch_hash=16b34eb974399935d3cf7c2526534f906991ccd876215176658347059cdd2021)(@babel/runtime@7.29.2): dependencies: '@babel/runtime': 7.29.2 dotenv: 16.6.1 - react-native-drawer-layout@4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(ac885d87795f1c83dff851cd6dfdabcc): - dependencies: + ? react-native-drawer-layout@4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(react-native-gesture-handler@2.32.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native-reanimated@4.6.0(92d1a74bc123f334ae9968fa1c80814f))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + : dependencies: color: 4.2.3 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-gesture-handler: 2.32.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-reanimated: 4.5.3(patch_hash=cb7a94c574c89fae48a259ea1ba4c4c97b1db634237336237805491b7c1234de)(c6ee2c3257efee1775b3ba0a37d05090) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-gesture-handler: 2.32.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-reanimated: 4.6.0(92d1a74bc123f334ae9968fa1c80814f) use-latest-callback: 0.2.6(react@19.2.3) - react-native-edge-to-edge@1.8.1(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-edge-to-edge@1.8.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-gesture-handler@2.32.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-gesture-handler@2.32.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: '@egjs/hammerjs': 2.0.17 '@types/react-test-renderer': 19.1.0 hoist-non-react-statics: 3.3.2 invariant: 2.2.4 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-is-edge-to-edge@1.3.1(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-is-edge-to-edge@1.3.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-keyboard-controller@1.21.9(patch_hash=ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271)(react-native-reanimated@4.5.3(patch_hash=cb7a94c574c89fae48a259ea1ba4c4c97b1db634237336237805491b7c1234de)(c6ee2c3257efee1775b3ba0a37d05090))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-keyboard-controller@1.21.9(patch_hash=ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271)(react-native-reanimated@4.6.0(92d1a74bc123f334ae9968fa1c80814f))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-is-edge-to-edge: 1.3.1(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-reanimated: 4.5.3(patch_hash=cb7a94c574c89fae48a259ea1ba4c4c97b1db634237336237805491b7c1234de)(c6ee2c3257efee1775b3ba0a37d05090) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-is-edge-to-edge: 1.3.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-reanimated: 4.6.0(92d1a74bc123f334ae9968fa1c80814f) - react-native-mmkv@3.3.3(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-mmkv@3.3.3(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-pager-view@6.8.0(patch_hash=c8316acd33c9aef6531e8c272c2bfab7bd02af1255969eeaab2bad5ab48531cc)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-pager-view@6.8.0(patch_hash=c8316acd33c9aef6531e8c272c2bfab7bd02af1255969eeaab2bad5ab48531cc)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-progress@5.0.1(react-native-svg@15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)): + react-native-progress@5.0.1(react-native-svg@15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)): dependencies: prop-types: 15.8.1 - react-native-svg: 15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-svg: 15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-qrcode-styled@0.3.3(react-native-svg@15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-qrcode-styled@0.3.3(react-native-svg@15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: qrcode: 1.5.4 react: 19.2.3 react-fast-compare: 3.2.2 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-svg: 15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-svg: 15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-reanimated@4.5.3(patch_hash=cb7a94c574c89fae48a259ea1ba4c4c97b1db634237336237805491b7c1234de)(c6ee2c3257efee1775b3ba0a37d05090): + react-native-reanimated@4.6.0(92d1a74bc123f334ae9968fa1c80814f): dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-is-edge-to-edge: 1.3.1(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-worklets: 0.11.3(patch_hash=8ebbcf528fc731459ac4a4eff612a50dfb7e24462a9df201e4aaa81a201343f0)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-is-edge-to-edge: 1.3.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-worklets: 0.12.1(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) semver: 7.8.0 - react-native-safe-area-context@5.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-safe-area-context@5.7.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-screens@4.26.2(patch_hash=ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-screens@4.26.2(patch_hash=ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) warn-once: 0.1.1 - react-native-svg@15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-svg@15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: css-select: 5.2.2 css-tree: 1.1.3 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) warn-once: 0.1.1 react-native-uuid@2.0.4: {} - react-native-view-shot@5.1.1(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-view-shot@5.1.1(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: html2canvas: 1.4.1 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) react-native-web-webview@1.0.2(file-loader@6.2.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): dependencies: @@ -18311,15 +18596,15 @@ snapshots: transitivePeerDependencies: - encoding - react-native-webview@13.17.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): + react-native-webview@13.17.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: '@typescript/native-preview': 7.0.0-dev.20260707.2 escape-string-regexp: 4.0.0 invariant: 2.2.4 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-worklets@0.11.3(patch_hash=8ebbcf528fc731459ac4a4eff612a50dfb7e24462a9df201e4aaa81a201343f0)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): + react-native-worklets@0.12.1(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1): dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.1 @@ -18337,12 +18622,12 @@ snapshots: '@react-native/metro-config': 0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) convert-source-map: 2.0.0 react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) semver: 7.8.0 transitivePeerDependencies: - supports-color - react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1): + react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1): dependencies: '@react-native/assets-registry': 0.86.0 '@react-native/codegen': 0.86.0(@babel/core@7.29.0(supports-color@8.1.1)) @@ -18350,7 +18635,7 @@ snapshots: '@react-native/gradle-plugin': 0.86.0 '@react-native/js-polyfills': 0.86.0 '@react-native/normalize-colors': 0.86.0 - '@react-native/virtualized-lists': 0.86.0(@types/react@19.2.18)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@react-native/virtualized-lists': 0.86.0(@types/react@19.2.18)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -18674,6 +18959,8 @@ snapshots: semver@7.8.0: {} + semver@7.8.5: {} + send@0.19.2(supports-color@8.1.1): dependencies: debug: 2.6.9(supports-color@8.1.1) @@ -18753,6 +19040,39 @@ snapshots: shallow-equal@3.1.0: {} + sharp@0.35.4(@types/node@24.12.4): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 24.12.4 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -18844,16 +19164,16 @@ snapshots: dependencies: atomic-sleep: 1.0.0 - sonner-native@0.26.4(669e6adb8ceb9d91fb46b2f510aae203): + sonner-native@0.26.4(a1115538eb9b7f01f00730be61406201): dependencies: react: 19.2.3 - react-native: 0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-gesture-handler: 2.32.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-reanimated: 4.5.3(patch_hash=cb7a94c574c89fae48a259ea1ba4c4c97b1db634237336237805491b7c1234de)(c6ee2c3257efee1775b3ba0a37d05090) - react-native-safe-area-context: 5.7.0(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-screens: 4.26.2(patch_hash=ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-svg: 15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) - react-native-worklets: 0.11.3(patch_hash=8ebbcf528fc731459ac4a4eff612a50dfb7e24462a9df201e4aaa81a201343f0)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.86.0(patch_hash=dd549527bb84c88acc7b0b1d521c9f4b666fda484c75cd0b282f8e9838524909)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) + react-native: 0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-gesture-handler: 2.32.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-reanimated: 4.6.0(92d1a74bc123f334ae9968fa1c80814f) + react-native-safe-area-context: 5.7.0(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-screens: 4.26.2(patch_hash=ba3295d44434a729f143a6b2c24e4a9b2f911ab200b2bc47e46cdfeaa1536f7c)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-svg: 15.15.4(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-worklets: 0.12.1(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.86.0(patch_hash=239208992c380d6785576d1fae2e3f60f60f509d7b09d56bf552be29d6c42a06)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/jest-preset@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1) sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b627e1d3a8..1bdc6c2632 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -17,9 +17,8 @@ overrides: '@react-native/normalize-colors': '0.86.0' '@expo/image-utils': '0.8.12' '@types/estree': '1.0.6' - 'react-native-compressor': '1.13.0' - 'react-native-reanimated': '4.5.3' - 'react-native-worklets': '0.11.3' + 'react-native-reanimated': '4.6.0' + 'react-native-worklets': '0.12.1' 'psl': '1.9.0' '@types/psl': '1.1.1' 'react-native-screens': '4.26.2' @@ -30,6 +29,7 @@ allowBuilds: 'esbuild': false 'unrs-resolver': true patchedDependencies: + '@formatjs/bigdecimal@0.2.5': patches/@formatjs__bigdecimal@0.2.5.patch '@sentry/expo-upload-sourcemaps@8.18.0': patches/@sentry__expo-upload-sourcemaps@8.18.0.patch 'expo-age-range@57.0.2': patches/expo-age-range@57.0.2.patch 'expo-haptics@57.0.1': patches/expo-haptics@57.0.1.patch @@ -38,16 +38,13 @@ patchedDependencies: 'expo-notifications@57.0.7': patches/expo-notifications@57.0.7.patch 'expo-updates@57.0.10': patches/expo-updates@57.0.10.patch expo@57.0.8: patches/expo@57.0.8.patch - 'react-native-compressor@1.13.0': patches/react-native-compressor@1.13.0.patch 'react-native-date-picker@5.0.13': patches/react-native-date-picker@5.0.13.patch 'react-native-dotenv@3.4.11': patches/react-native-dotenv@3.4.11.patch 'react-native-drawer-layout@4.2.3': patches/react-native-drawer-layout@4.2.3.patch 'react-native-keyboard-controller@1.21.9': patches/react-native-keyboard-controller@1.21.9.patch 'react-native-pager-view@6.8.0': patches/react-native-pager-view@6.8.0.patch - 'react-native-reanimated@4.5.3': patches/react-native-reanimated@4.5.3.patch 'react-native-screens@4.26.2': patches/react-native-screens@4.26.2.patch 'react-native-svg@15.15.4': patches/react-native-svg@15.15.4.patch - react-native-worklets@0.11.3: patches/react-native-worklets@0.11.3.patch 'react-native@0.86.0': patches/react-native@0.86.0.patch minimumReleaseAgeExclude: - '@atproto/*' diff --git a/src/App.tsx b/src/App.tsx index 2af2146d92..2f0a702039 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -126,9 +126,8 @@ function InnerApp() { } } catch (e) { logger.warn(`session: resume failed`, {message: e}) - } finally { - setIsReady(true) } + setIsReady(true) } const account = readLastActiveAccount() void onLaunch(account) diff --git a/src/App.web.tsx b/src/App.web.tsx index 8070c4d333..ef6ac5e4c5 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -105,9 +105,8 @@ function InnerApp() { } } catch (e) { logger.warn('session: resumeSession failed', {message: e}) - } finally { - setIsReady(true) } + setIsReady(true) } const account = readLastActiveAccount() void onLaunch(account) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index c03e65af69..86295f3795 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -80,6 +80,7 @@ import {FindContactsFlowScreen} from '#/screens/FindContactsFlowScreen' import HashtagScreen from '#/screens/Hashtag' import {LogScreen} from '#/screens/Log' import {MessagesScreen} from '#/screens/Messages/ChatList' +import {renderMessagesSplitViewLayout} from '#/screens/Messages/components/splitView/MessagesSplitViewLayout' import {MessagesConversationScreen} from '#/screens/Messages/Conversation' import {MessagesConversationSettingsScreen} from '#/screens/Messages/ConversationSettings' import {MessagesInboxScreen} from '#/screens/Messages/Inbox' @@ -88,6 +89,9 @@ import {MessagesSettingsScreen} from '#/screens/Messages/Settings' import {ModerationScreen} from '#/screens/Moderation' import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings' import {ModerationInboxScreen} from '#/screens/ModerationInbox' +import {ModerationInboxReportDetailsScreen} from '#/screens/ModerationInbox/Report' +import {ModerationInboxSettingsScreen} from '#/screens/ModerationInbox/Settings' +import {ModerationInboxSubjectDetailsScreen} from '#/screens/ModerationInbox/Subject' import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings' import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList' import {PostLikedByScreen} from '#/screens/Post/PostLikedBy' @@ -140,7 +144,6 @@ import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' import {InviteScannerScreen} from '#/features/inviteFriends' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' -import {renderMessagesSplitViewLayout} from './screens/Messages/components/splitView/MessagesSplitViewLayout' const navigationRef = createNavigationContainerRef() @@ -184,6 +187,21 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { getComponent={() => ModerationInboxScreen} options={{title: title(msg`Moderation inbox`), requireAuth: true}} /> + ModerationInboxSettingsScreen} + options={{title: title(msg`Mod inbox settings`), requireAuth: true}} + /> + ModerationInboxReportDetailsScreen} + options={{title: title(msg`Your report`), requireAuth: true}} + /> + ModerationInboxSubjectDetailsScreen} + options={{title: title(msg`Notice`), requireAuth: true}} + /> ModerationModlistsScreen} @@ -563,12 +581,12 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { Wizard} - options={{title: title(msg`Create a starter pack`), requireAuth: true}} + options={{title: title(msg`Create a Starter Pack`), requireAuth: true}} /> Wizard} - options={{title: title(msg`Edit your starter pack`), requireAuth: true}} + options={{title: title(msg`Edit your Starter Pack`), requireAuth: true}} /> void refetch()} + isRetrying={isFetching} + secondaryAction={{ + label: l`Sign out`, + onPress: () => { + if (IS_WEB) history.pushState(null, '', '/') + logoutCurrentAccount('AgeAssuranceDataUnavailableScreen') + }, + }} + /> + ) +} diff --git a/src/ageAssurance/components/NoAccessScreen.tsx b/src/ageAssurance/components/NoAccessScreen.tsx index 895e57d077..a1014cc0b5 100644 --- a/src/ageAssurance/components/NoAccessScreen.tsx +++ b/src/ageAssurance/components/NoAccessScreen.tsx @@ -1,4 +1,4 @@ -import {useCallback, useEffect} from 'react' +import {useCallback, useEffect, useEffectEvent} from 'react' import {ScrollView, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Trans, useLingui} from '@lingui/react/macro' @@ -76,7 +76,7 @@ export function NoAccessScreen() { const geolocationString = createGeolocationString(geolocation, i18n.locale) const isUsingGPS = !!geolocation.deviceGeolocation?.countryCode && IS_NATIVE - useEffect(() => { + const onShown = useEffectEvent(() => { // just counting overall hits here ax.metric(`blockedGeoOverlay:shown`, {}) ax.metric(`ageAssurance:noAccessScreen:shown`, { @@ -85,8 +85,10 @@ export function NoAccessScreen() { hasDeclaredAge, canUpdateBirthday, }) - // TODO This can be cleaned up with useEffectEvent once we're on 19.2 - // eslint-disable-next-line react-hooks/exhaustive-deps + }) + + useEffect(() => { + onShown() }, []) const onPressLogout = useCallback(() => { diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index 369b9eedd7..a2069ddc00 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -7,7 +7,7 @@ import {focusManager, QueryClient, useQuery} from '@tanstack/react-query' import {persistQueryClient} from '@tanstack/react-query-persist-client' import debounce from 'lodash.debounce' -import {networkRetry} from '#/lib/async/retry' +import {isRetryableRequestError, networkRetry} from '#/lib/async/retry' import {createPersistedQueryStorage} from '#/lib/persisted-query-storage' import {getAge} from '#/lib/strings/time' import { @@ -347,9 +347,15 @@ export type OtherRequiredData = { birthdate: string | undefined actorDeclaration?: chat.bsky.actor.declaration.Main } +export type OtherRequiredDataStatus = 'pending' | 'error' | 'success' +const otherRequiredDataRetryOptions = { + retry: (failureCount: number, error: unknown) => + failureCount < 2 && isRetryableRequestError(error), +} export function createOtherRequiredDataQueryKey({did}: {did: string}) { return ['otherRequiredData', did] } + async function getOtherRequiredData({ accountClient, }: { @@ -455,10 +461,11 @@ export async function prefetchOtherRequiredData({ try { logger.debug(`prefetchOtherRequiredData: resolving...`) - const res = await networkRetry(3, () => - getOtherRequiredData({accountClient}), - ) - qc.setQueryData(qk, res) + await qc.fetchQuery({ + ...otherRequiredDataRetryOptions, + queryKey: qk, + queryFn: () => getOtherRequiredData({accountClient}), + }) } catch (err) { const e = err as Error logger.warn(`prefetchOtherRequiredData: failed`, { @@ -490,12 +497,14 @@ export function useOtherRequiredDataQuery() { const did = accountClient.did return useQuery( { + ...otherRequiredDataRetryOptions, enabled: !!did, initialData: () => { if (!did) return return getOtherRequiredDataFromCache({did}) }, queryKey: createOtherRequiredDataQueryKey({did: did!}), + retryOnMount: false, async queryFn() { return getOtherRequiredData({accountClient}) }, @@ -722,6 +731,11 @@ export type AgeAssuranceServerData = { */ state: app.bsky.ageassurance.defs.State | undefined metadata: AgeAssuranceMetadata | undefined + /** + * Whether the account data needed to compute age assurance is available. + * A successful response without a birthdate is still `success`. + */ + otherRequiredDataStatus: OtherRequiredDataStatus /** * The native on-device age signals for the region the user is currently in, * if they've granted access there. Already resolved from the region-keyed @@ -739,6 +753,7 @@ const AgeAssuranceServerDataContext = createContext({ declaredAge: undefined, birthdate: undefined, }, + otherRequiredDataStatus: 'pending', deviceSignals: undefined, }) export function useAgeAssuranceServerDataContext() { @@ -752,7 +767,18 @@ export function AgeAssuranceServerDataProvider({ const {data: config} = useConfigQuery() const serverState = useServerStateQuery() const {state, metadata} = serverState.data || {} - const {data} = useOtherRequiredDataQuery() + const {data, errorUpdatedAt, status} = useOtherRequiredDataQuery() + /* + * A data-less query returns to `pending` and clears `error` while refetching, + * but retains `errorUpdatedAt`. Keep the error screen mounted until data + * loads successfully. + */ + const otherRequiredDataStatus: OtherRequiredDataStatus = + data !== undefined + ? 'success' + : status === 'error' || errorUpdatedAt > 0 + ? 'error' + : 'pending' // `select` resolves the cached region-keyed map to the current region. const {data: deviceSignals} = useDeviceSignalsQuery() const ctx = useMemo( @@ -767,9 +793,10 @@ export function AgeAssuranceServerDataProvider({ : undefined, birthdate: data?.birthdate, }, + otherRequiredDataStatus, deviceSignals, }), - [config, state, data, metadata, deviceSignals], + [config, state, data, metadata, otherRequiredDataStatus, deviceSignals], ) return ( diff --git a/src/ageAssurance/state.test.ts b/src/ageAssurance/state.test.ts new file mode 100644 index 0000000000..66b0fb644a --- /dev/null +++ b/src/ageAssurance/state.test.ts @@ -0,0 +1,85 @@ +import {computeAgeAssuranceState} from '#/ageAssurance/state' +import {AgeAssuranceAccess, AgeAssuranceStatus} from '#/ageAssurance/types' + +jest.mock('#/ageAssurance/data', () => ({})) +jest.mock('#/ageAssurance/logger', () => ({ + logger: { + debug: jest.fn(), + warn: jest.fn(), + }, +})) +jest.mock('#/state/session', () => ({})) + +const geolocation = { + countryCode: undefined, + regionCode: undefined, +} + +describe('computeAgeAssuranceState', () => { + it('computes access while required account data is pending', () => { + expect( + computeAgeAssuranceState({ + hasSession: true, + geolocation, + config: {regions: []}, + otherRequiredDataStatus: 'pending', + }), + ).toMatchObject({ + status: AgeAssuranceStatus.Unknown, + access: AgeAssuranceAccess.None, + }) + }) + + it('denies access when required account data fails', () => { + expect( + computeAgeAssuranceState({ + hasSession: true, + geolocation, + config: {regions: []}, + otherRequiredDataStatus: 'error', + }), + ).toEqual({ + status: AgeAssuranceStatus.Unknown, + access: AgeAssuranceAccess.None, + error: 'account-data', + }) + }) + + it('computes access after a successful response without a birthdate', () => { + expect( + computeAgeAssuranceState({ + hasSession: true, + geolocation, + config: {regions: []}, + metadata: {birthdate: undefined}, + otherRequiredDataStatus: 'success', + }), + ).toMatchObject({ + status: AgeAssuranceStatus.Unknown, + access: AgeAssuranceAccess.None, + }) + }) + + it('preserves authoritative terminal server state without account data', () => { + expect( + computeAgeAssuranceState({ + hasSession: true, + geolocation: {countryCode: 'AA', regionCode: undefined}, + config: { + regions: [ + { + countryCode: 'AA', + minAccessAge: 13, + rules: [], + }, + ], + }, + state: {status: 'blocked', access: 'none'}, + otherRequiredDataStatus: 'error', + }), + ).toMatchObject({ + status: AgeAssuranceStatus.Blocked, + access: AgeAssuranceAccess.None, + }) + }) +}) diff --git a/src/ageAssurance/state.ts b/src/ageAssurance/state.ts index fb799f934e..a054c0bc1f 100644 --- a/src/ageAssurance/state.ts +++ b/src/ageAssurance/state.ts @@ -9,6 +9,7 @@ import { getDeviceSignalsFromCacheForRegion, getOtherRequiredDataFromCache, getServerStateFromCache, + type OtherRequiredDataStatus, useAgeAssuranceServerDataContext, } from '#/ageAssurance/data' import {logger} from '#/ageAssurance/logger' @@ -35,12 +36,13 @@ import {device} from '#/storage' * server state before computing access based on AA config from the server + * geolocation and other data. */ -function computeAgeAssuranceState({ +export function computeAgeAssuranceState({ hasSession, geolocation, config, state, metadata, + otherRequiredDataStatus, deviceSignals, }: { hasSession: boolean @@ -48,6 +50,7 @@ function computeAgeAssuranceState({ config?: app.bsky.ageassurance.defs.Config state?: app.bsky.ageassurance.defs.State metadata?: AgeAssuranceMetadata + otherRequiredDataStatus: OtherRequiredDataStatus deviceSignals?: AgeRange.AgeRangeResponse }) { /** @@ -93,6 +96,14 @@ function computeAgeAssuranceState({ } } + if (otherRequiredDataStatus === 'error') { + return { + status: AgeAssuranceStatus.Unknown, + access: AgeAssuranceAccess.None, + error: 'account-data' as const, + } + } + /* * Otherwise, we need to compute the access based on the latest data. For * accounts with an accurate birthdate, our default fallback rules should @@ -177,6 +188,7 @@ export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) { geolocation, state: state.state, metadata, + otherRequiredDataStatus: 'success', deviceSignals, }) @@ -194,7 +206,7 @@ export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) { export function useAgeAssuranceState(): AgeAssuranceState { const {hasSession} = useSession() const geolocation = useGeolocation() - const {config, state, metadata, deviceSignals} = + const {config, state, metadata, otherRequiredDataStatus, deviceSignals} = useAgeAssuranceServerDataContext() return useMemo( @@ -205,9 +217,18 @@ export function useAgeAssuranceState(): AgeAssuranceState { geolocation, state, metadata, + otherRequiredDataStatus, deviceSignals, }), - [hasSession, geolocation, config, state, metadata, deviceSignals], + [ + hasSession, + geolocation, + config, + state, + metadata, + otherRequiredDataStatus, + deviceSignals, + ], ) } diff --git a/src/ageAssurance/types.ts b/src/ageAssurance/types.ts index b9b775396a..b2eba6ac73 100644 --- a/src/ageAssurance/types.ts +++ b/src/ageAssurance/types.ts @@ -43,7 +43,8 @@ export type AgeAssuranceState = { lastInitiatedAt?: string status: AgeAssuranceStatus access: AgeAssuranceAccess - error?: 'config' // maybe other specific cases in the future + isLoading?: boolean + error?: 'config' | 'account-data' } export type AgeAssuranceFlags = { diff --git a/src/alf/util/useGutters.ts b/src/alf/util/useGutters.ts index f7dcef51bd..07ee11e658 100644 --- a/src/alf/util/useGutters.ts +++ b/src/alf/util/useGutters.ts @@ -1,5 +1,3 @@ -import {useMemo} from 'react' - import {type Breakpoint, useBreakpoints} from '#/alf/breakpoints' import * as tokens from '#/alf/tokens' @@ -44,23 +42,26 @@ export function useGutters([top, right, bottom, left]: [ Gutter, Gutter, ]): Gutters -export function useGutters([top, right, bottom, left]: Gutter[]) { +export function useGutters(gutter: Gutter[]) { const {activeBreakpoint} = useBreakpoints() + /* + * Destructured in the body rather than the parameter list: these are + * reassigned below to fill in the CSS-shorthand forms, and React Compiler + * cannot lower a reassigned destructured parameter. + */ + let [top, right, bottom, left] = gutter if (right === undefined) { right = bottom = left = top } else if (bottom === undefined) { bottom = top left = right } - return useMemo(() => { - return { - paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'], - paddingRight: - right === 0 ? 0 : gutters[right][activeBreakpoint || 'default'], - paddingBottom: - bottom === 0 ? 0 : gutters[bottom][activeBreakpoint || 'default'], - paddingLeft: - left === 0 ? 0 : gutters[left][activeBreakpoint || 'default'], - } - }, [activeBreakpoint, top, right, bottom, left]) + return { + paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'], + paddingRight: + right === 0 ? 0 : gutters[right][activeBreakpoint || 'default'], + paddingBottom: + bottom === 0 ? 0 : gutters[bottom][activeBreakpoint || 'default'], + paddingLeft: left === 0 ? 0 : gutters[left][activeBreakpoint || 'default'], + } } diff --git a/src/analytics/features/index.ts b/src/analytics/features/index.ts index 6cbafeaa22..42fb3e082b 100644 --- a/src/analytics/features/index.ts +++ b/src/analytics/features/index.ts @@ -77,21 +77,19 @@ export function getFeatures() { export function getFeatureDescription(feature: Features, i18n: I18n) { switch (feature) { - case Features.CanonicalPostNumberingEnable: + case Features.PostFeedKnownLikersEnable: return { key: feature, name: i18n._( msg({ - message: 'Thread numbering', - comment: - 'Name for a feature flag (See numbered badges (1/3, 2/3, etc.) on posts in a thread by the same author.)', + message: 'Social proofing on posts', + comment: 'Name for a feature flag', }), ), description: i18n._( msg({ - message: - 'See numbered badges (1/3, 2/3, etc.) on posts in a thread by the same author.', - comment: 'Description of a feature flag (Thread numbering)', + message: 'Spot posts your friends and follows have liked.', + comment: 'Description of a feature flag (Social proofing on posts)', }), ), } diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index 4d3cf5e197..d05a1e59c1 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -16,12 +16,12 @@ export enum Features { GroupChatsDisable = 'group_chats:disable', ComposerLanguageDetectionEnable = 'composer:language_detection:enable', PostGalleryEmbedEnable = 'post_gallery_embed:enable', + PostFeedKnownLikersEnable = 'post_feed:known_likers:enable', PostThreadKnownLikersEnable = 'post_thread:known_likers:enable', CustomLogoJapanEnable = 'custom_logo:japan:enable', SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable', FollowSortEnable = 'follow_sort:enable', OnboardingInterestsRequiredEnable = 'onboarding:interests:required:enable', - CanonicalPostNumberingEnable = 'canonical_post_numbering:enable', ContentVisibilitySettingsEnable = 'content_visibility_settings:enable', ModerationInboxEnable = 'moderation_inbox:enable', diff --git a/src/analytics/index.tsx b/src/analytics/index.tsx index 07af41473e..ac1c96e629 100644 --- a/src/analytics/index.tsx +++ b/src/analytics/index.tsx @@ -117,6 +117,7 @@ const Context = createContext({ geolocation: device.get(['geolocationServiceResponse']) || { countryCode: '', regionCode: '', + city: '', }, }, }) @@ -181,6 +182,7 @@ export function AnalyticsContext({ } const deviceId = useDeviceId() ?? 'unknown' const sessionId = useSessionId() + // only IP based, never GPS const geolocation = useGeolocationServiceResponse() const parentContext = useContext(Context) /* diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 78a4574f0f..42e9ec2124 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -44,6 +44,7 @@ export type Events = { | 'SignupQueued' | 'Deactivated' | 'Takendown' + | 'AgeAssuranceDataUnavailableScreen' | 'AgeAssuranceNoAccessScreen' scope: 'current' | 'every' } @@ -450,6 +451,7 @@ export type Events = { 'post:view': { uri: string authorDid: string + isReply: boolean logContext: | 'FeedItem' | 'PostThreadItem' @@ -703,6 +705,7 @@ export type Events = { } 'starterPack:removeUser': { starterPack?: string + context?: 'opt-out' } 'starterPack:share': { starterPack: string @@ -715,6 +718,10 @@ export type Events = { count: number } 'starterPack:delete': {} + 'starterPack:optOut': { + starterPack: string + action: 'optOut' | 'undo' + } 'starterPack:create': { setName: boolean setDescription: boolean @@ -1396,6 +1403,41 @@ export type Events = { playlist: string } + /** + * The playable video was meaningfully visible. This is an exposure event, + * not proof that playback started. Fires once per mounted video item. + */ + 'video:impression': { + postUri?: string + postAuthorDid?: string + context: 'embed' | 'immersiveFeed' + presentation: 'video' | 'gif' + } + /** + * Playback advanced far enough to render the first frame. Preloading and + * merely becoming active do not count. Fires once per mounted video item; + * automatic loops do not produce another event. + */ + 'video:playback:start': { + postUri?: string + postAuthorDid?: string + context: 'embed' | 'immersiveFeed' + presentation: 'video' | 'gif' + autoplay: boolean + } + /** + * The user activated a third-party media player. Cross-origin players do + * not expose confirmed playback consistently, so this must not be treated + * as equivalent to video:playback:start without an explicit methodology. + */ + 'externalEmbed:playerActivated': { + postUri?: string + postAuthorDid?: string + source: string + playerType: string + mediaType: 'video' | 'audio' | 'gif' | 'other' + } + // === Video upload funnel (Frontend Spec section D) === // Every event carries uploadId (client-generated UUID, ties one upload // session end-to-end) + engine (compression engine id, e.g. diff --git a/src/analytics/utils.ts b/src/analytics/utils.ts index b20b3719c9..af1ef84070 100644 --- a/src/analytics/utils.ts +++ b/src/analytics/utils.ts @@ -11,14 +11,18 @@ import { * Thin `useMemo` wrapper that marks the metadata as memoized and provides a * type guard. */ -export function useMeta(metadata?: MergeableMetadata) { - const m = useMemo(() => metadata, [metadata]) - if (!m) return +function markMemoized(m: T): T { // @ts-expect-error m.__meta = true return m } +export function useMeta(metadata?: MergeableMetadata) { + const m = useMemo(() => metadata, [metadata]) + if (!m) return + return markMemoized(m) +} + export function accountToSessionMetadata( account: SessionAccount | undefined, ): SessionMetadata | undefined { diff --git a/src/components/AvatarStack.tsx b/src/components/AvatarStack.tsx index 8c5893ed76..486a025920 100644 --- a/src/components/AvatarStack.tsx +++ b/src/components/AvatarStack.tsx @@ -13,13 +13,17 @@ export function AvatarStack({ size = 26, numPending, backgroundColor, + borderWidth = 1, + overlap, }: { profiles: bsky.profile.AnyProfileView[] size?: number numPending?: number backgroundColor?: string + borderWidth?: number + overlap?: number }) { - const translation = size / 3 // overlap by 1/3 + const translation = overlap ?? size / 3 const t = useTheme() const moderationOpts = useModerationOpts() @@ -55,7 +59,7 @@ export function AvatarStack({ width: size, height: size, left: i * -translation, - borderWidth: 1, + borderWidth, borderColor: backgroundColor ?? t.atoms.bg.backgroundColor, borderRadius: 999, zIndex: 3 - i, @@ -63,7 +67,7 @@ export function AvatarStack({ ]}> {item.profile && ( { if (IS_WEB) { - inputScrollSharedValue.value = (e.target as any).scrollTop + inputScrollSharedValue.set((e.target as any).scrollTop) } else { - inputScrollSharedValue.value = e.nativeEvent.contentOffset.y + inputScrollSharedValue.set(e.nativeEvent.contentOffset.y) } }} // @ts-expect-error web only diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx index 95492a385a..294ddbd96e 100644 --- a/src/components/ContextMenu/index.tsx +++ b/src/components/ContextMenu/index.tsx @@ -530,7 +530,7 @@ export function AuxiliaryView({ transform: [ { translateY: - (ensureOnScreenTranslationSV.get() || translationSV.get()) * + Math.max(ensureOnScreenTranslationSV.get(), translationSV.get()) * animationSV.get(), }, {scale: interpolate(animationSV.get(), [0, 1], [0.2, 1])}, @@ -544,21 +544,17 @@ export function AuxiliaryView({ const onLayout = useCallback(() => { if (!measurement) return - let translation = 0 - // vibes based, just assuming it'll fit within this space. revisit if we use // AuxiliaryView for something tall const TOP_INSET = topInset + 80 const distanceMessageFromTop = measurement.y - TOP_INSET - if (distanceMessageFromTop < 0) { - translation = -distanceMessageFromTop - } + const minimumTranslation = -distanceMessageFromTop // normally, the context menu is responsible for measuring itself and moving everything into the right place // however, in auxiliary-only mode, that doesn't happen, so we need to do it ourselves here if (mode === 'auxiliary-only') { - translationSV.set(translation) + translationSV.set(Math.max(minimumTranslation, 0)) ensureOnScreenTranslationSV.set(0) } // however, we also need to make sure that for super tall triggers, we don't go off the screen @@ -567,7 +563,7 @@ export function AuxiliaryView({ // we'll just have to live with it for now, fixing it would be possible but be a large complexity // increase for an edge case else { - ensureOnScreenTranslationSV.set(translation) + ensureOnScreenTranslationSV.set(minimumTranslation) } }, [mode, measurement, translationSV, topInset, ensureOnScreenTranslationSV]) diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index 5c36af6ae5..93f741432b 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -42,7 +42,6 @@ export function useDialogControl(): DialogOuterProps['control'] { useEffect(() => { activeDialogs.current.set(id, control) return () => { - // eslint-disable-next-line react-hooks/exhaustive-deps activeDialogs.current.delete(id) } }, [id, activeDialogs]) diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index e867acd60a..ee74da6249 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -73,12 +73,18 @@ export function Outer({ setIsOpen(false) try { - if (cb && typeof cb === 'function') { - // 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) + /* + * Nested rather than `&&`: React Compiler cannot lower a logical + * expression in a test position inside a `try`. + */ + if (cb) { + if (typeof cb === 'function') { + // 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) { logger.error(`Dialog closeCallback failed`, { diff --git a/src/components/EmojiPicker/preload.web.ts b/src/components/EmojiPicker/preload.web.ts index 4456153b71..13f52b996b 100644 --- a/src/components/EmojiPicker/preload.web.ts +++ b/src/components/EmojiPicker/preload.web.ts @@ -1,4 +1,3 @@ -import {useCallback} from 'react' import {init} from 'emoji-mart' /** @@ -16,15 +15,16 @@ let loadRequested = false * * @see {@link https://github.com/missive/emoji-mart/blob/16978d04a766eec6455e2e8bb21cd8dc0b3c7436/README.md?plain=1#L194 | emoji-mart preloading docs} */ -export function useWebPreloadEmoji({immediate}: {immediate?: boolean} = {}) { - const preload = useCallback(async () => { - if (loadRequested) return - loadRequested = true - try { - const data = (await import('@emoji-mart/data')).default - init({data}) - } catch (e) {} - }, []) - if (immediate) preload() - return preload +async function loadEmojiData() { + if (loadRequested) return + loadRequested = true + try { + const data = (await import('@emoji-mart/data')).default + init({data}) + } catch (e) {} +} + +export function useWebPreloadEmoji({immediate}: {immediate?: boolean} = {}) { + if (immediate) loadEmojiData() + return loadEmojiData } diff --git a/src/components/Error.tsx b/src/components/Error.tsx index 190165a2df..a8739ce35a 100644 --- a/src/components/Error.tsx +++ b/src/components/Error.tsx @@ -3,27 +3,38 @@ import {Trans, useLingui} from '@lingui/react/macro' import {useGoBack} from '#/lib/hooks/useGoBack' import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonText} from '#/components/Button' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {type Props as SVGIconProps} from '#/components/icons/common' import * as Layout from '#/components/Layout' +import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' export function Error({ + icon: Icon, title, message, onRetry, onGoBack, hideBackButton, + secondaryAction, + isRetrying, }: { + icon?: React.ComponentType title?: string message?: string onRetry?: () => unknown onGoBack?: () => unknown hideBackButton?: boolean + isRetrying?: boolean + secondaryAction?: { + label: string + accessibilityLabel?: string + onPress: () => unknown + } }) { const {t: l} = useLingui() const t = useTheme() const {gtMobile} = useBreakpoints() - const goBack = useGoBack(onGoBack) return ( - - {title} + + {Icon && } + + {title} + {onRetry && ( )} - {!hideBackButton && ( + {!hideBackButton && secondaryAction ? ( - )} + ) : !hideBackButton ? ( + + ) : null} ) } + +function GoBackButton({ + hasRetry, + isRetrying, + onGoBack, +}: { + hasRetry: boolean + isRetrying?: boolean + onGoBack?: () => unknown +}) { + const {t: l} = useLingui() + const goBack = useGoBack(onGoBack) + + return ( + + ) +} diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index bf55f218d6..eb9dee75f3 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -295,6 +295,8 @@ function SaveButtonInner({ e.preventDefault() e.stopPropagation() + const pinned = pin || false + try { if (savedFeedConfig) { await removeFeed(savedFeedConfig) @@ -303,7 +305,7 @@ function SaveButtonInner({ { type, value: uri, - pinned: pin || false, + pinned, }, ]) } diff --git a/src/components/InterestTabs.tsx b/src/components/InterestTabs.tsx index 5a99a36992..ca4d035393 100644 --- a/src/components/InterestTabs.tsx +++ b/src/components/InterestTabs.tsx @@ -54,6 +54,16 @@ export function InterestTabs({ const pendingTabOffsets = useRef<{x: number; width: number}[]>([]) const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([]) + function scrollIntoViewIfNeeded(index: number) { + const btnLayout = tabOffsets[index] + if (!btnLayout) return + listRef.current?.scrollTo({ + // centered + x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2), + animated: true, + }) + } + const onInitialLayout = useNonReactiveCallback(() => { const index = interests.indexOf(selectedInterest) scrollIntoViewIfNeeded(index) @@ -65,16 +75,6 @@ export function InterestTabs({ } }, [tabOffsets, onInitialLayout]) - function scrollIntoViewIfNeeded(index: number) { - const btnLayout = tabOffsets[index] - if (!btnLayout) return - listRef.current?.scrollTo({ - // centered - x: btnLayout.x - (totalWidth / 2 - btnLayout.width / 2), - animated: true, - }) - } - function handleSelectTab(index: number) { const tab = interests[index] onSelectTab(tab) diff --git a/src/components/LanguageSelect.tsx b/src/components/LanguageSelect.tsx index 6bcaebe539..0dd600feb3 100644 --- a/src/components/LanguageSelect.tsx +++ b/src/components/LanguageSelect.tsx @@ -5,13 +5,15 @@ import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {APP_LANGUAGES} from '#/locale/languages' import * as Select from '#/components/Select' +const DEFAULT_ITEMS = APP_LANGUAGES.map(l => ({ + label: l.name, + value: l.code2, +})) + export function LanguageSelect({ value, onChange, - items = APP_LANGUAGES.map(l => ({ - label: l.name, - value: l.code2, - })), + items = DEFAULT_ITEMS, label, disabledBlueskySupportedLanguageSanitization = false, }: { diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx index c70cebd703..7f3969adcd 100644 --- a/src/components/Layout/Header/index.tsx +++ b/src/components/Layout/Header/index.tsx @@ -219,7 +219,7 @@ export function SubtitleText({children}: {children: React.ReactNode}) { IS_IOS && align === 'platform' && a.text_center, t.atoms.text_contrast_medium, ]} - numberOfLines={2}> + numberOfLines={1}> {children} ) diff --git a/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx b/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx index 72d39875ab..6227eeb588 100644 --- a/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx +++ b/src/components/Lightbox/pager/ImageItem/ImageItem.android.tsx @@ -79,6 +79,11 @@ const ImageItem = ({ // Keep track of when we're entering or leaving scaled rendering. // Note: DO NOT move any logic reading animated values outside this function. + function handleZoom(nextIsScaled: boolean) { + setIsScaled(nextIsScaled) + onZoom(nextIsScaled) + } + useAnimatedReaction( () => { if (pinchScale.get() !== 1) { @@ -100,11 +105,6 @@ const ImageItem = ({ }, ) - function handleZoom(nextIsScaled: boolean) { - setIsScaled(nextIsScaled) - onZoom(nextIsScaled) - } - // On Android, stock apps prevent going "out of bounds" on pan or pinch. You should "bump" into edges. // If the user tried to pan too hard, this function will provide the negative panning to stay in bounds. function getExtraTranslationToStayInBounds( diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx index fc499a7fa0..1874eeff41 100644 --- a/src/components/NewskieDialog.tsx +++ b/src/components/NewskieDialog.tsx @@ -102,7 +102,7 @@ function DialogInner({ if (isMe) { if (profile.joinedViaStarterPack) { return _( - msg`You joined Bluesky using a starter pack ${timeAgoString} ago`, + msg`You joined Bluesky using a Starter Pack ${timeAgoString} ago`, ) } else { return _(msg`You joined Bluesky ${timeAgoString} ago`) @@ -110,7 +110,7 @@ function DialogInner({ } else { if (profile.joinedViaStarterPack) { return _( - msg`${profileName} joined Bluesky using a starter pack ${timeAgoString} ago`, + msg`${profileName} joined Bluesky using a Starter Pack ${timeAgoString} ago`, ) } else { return _(msg`${profileName} joined Bluesky ${timeAgoString} ago`) diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx index 63c4236aa1..9f47bc8f1e 100644 --- a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx +++ b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx @@ -22,6 +22,7 @@ import {useNavigation} from '@react-navigation/native' import {type NavigationProp} from '#/lib/routes/types' import { type EmbedPlayerParams, + getEmbedPlayerMediaType, getPlayerAspect, } from '#/lib/strings/embed-player' import {useExternalEmbedsPrefs} from '#/state/preferences' @@ -32,6 +33,7 @@ import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' import {Fill} from '#/components/Fill' import {KeepAwake} from '#/components/KeepAwake' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' +import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' import {type app} from '#/lexicons' @@ -121,9 +123,11 @@ function Player({ export function ExternalPlayer({ link, params, + post, }: { link: app.bsky.embed.external.ViewExternal params: EmbedPlayerParams + post?: app.bsky.feed.defs.PostView }) { const t = useTheme() const navigation = useNavigation() @@ -131,10 +135,31 @@ export function ExternalPlayer({ const windowDims = useWindowDimensions() const externalEmbedsPrefs = useExternalEmbedsPrefs() const consentDialogControl = useDialogControl() + const ax = useAnalytics() const [isPlayerActive, setIsPlayerActive] = useState(false) const [isLoading, setIsLoading] = useState(true) + const activatePlayer = useCallback(() => { + if (!isPlayerActive) { + ax.metric('externalEmbed:playerActivated', { + postUri: post?.uri, + postAuthorDid: post?.author.did, + source: params.source, + playerType: params.type, + mediaType: getEmbedPlayerMediaType(params.type), + }) + } + setIsPlayerActive(true) + }, [ + ax, + isPlayerActive, + params.source, + params.type, + post?.author.did, + post?.uri, + ]) + const aspect = useMemo(() => { return getPlayerAspect({ type: params.type, @@ -202,14 +227,14 @@ export function ExternalPlayer({ return } - setIsPlayerActive(true) + activatePlayer() }, - [externalEmbedsPrefs, consentDialogControl, params.source], + [externalEmbedsPrefs, consentDialogControl, params.source, activatePlayer], ) const onAcceptConsent = useCallback(() => { - setIsPlayerActive(true) - }, []) + activatePlayer() + }, [activatePlayer]) return ( <> diff --git a/src/components/Post/Embed/ExternalEmbed/index.tsx b/src/components/Post/Embed/ExternalEmbed/index.tsx index a496780524..64940d710c 100644 --- a/src/components/Post/Embed/ExternalEmbed/index.tsx +++ b/src/components/Post/Embed/ExternalEmbed/index.tsx @@ -27,11 +27,13 @@ import {GifEmbed} from './Gif' export const ExternalEmbed = ({ link, onOpen, + post, style, hideAlt, }: { link: app.bsky.embed.external.ViewExternal onOpen?: () => void + post?: app.bsky.feed.defs.PostView style?: StyleProp hideAlt?: boolean }) => { @@ -120,7 +122,11 @@ export const ExternalEmbed = ({ {embedPlayerParams?.isGif ? ( ) : embedPlayerParams ? ( - + ) : undefined} void}> @@ -33,6 +35,7 @@ export function VideoEmbedInnerNative({ setStatus: (status: 'playing' | 'paused') => void setIsLoading: (isLoading: boolean) => void setIsActive: (isActive: boolean) => void + onPlaybackStart: (autoplay: boolean) => void /** * Called with the native error message before the component throws to the * surrounding error boundary. @@ -46,6 +49,7 @@ export function VideoEmbedInnerNative({ const [muted, setMuted] = useVideoMuteState() const reportDialogMetadata = useReportDialogMetadataContext() const maxTimeRemainingSeconds = useRef(0) + const playbackStartTrackedRef = useRef(false) const [isPlaying, setIsPlaying] = useState(false) const [timeRemaining, setTimeRemaining] = useState(0) @@ -62,12 +66,13 @@ export function VideoEmbedInnerNative({ } const isGif = embed.presentation === 'gif' + const autoplay = !autoplayDisabled && !isWithinMessage return ( { @@ -88,20 +93,26 @@ export function VideoEmbedInnerNative({ onTimeRemainingChange={e => { const {timeRemaining} = e.nativeEvent setTimeRemaining(timeRemaining) - if ( - !isGif && - reportDialogMetadata && - Number.isFinite(timeRemaining) && - timeRemaining >= 0 - ) { + if (Number.isFinite(timeRemaining) && timeRemaining >= 0) { maxTimeRemainingSeconds.current = Math.max( maxTimeRemainingSeconds.current, timeRemaining, ) - reportDialogMetadata.current.videoTimestampSeconds = Math.max( - 0, - maxTimeRemainingSeconds.current - timeRemaining, - ) + if ( + !playbackStartTrackedRef.current && + hasPlaybackStarted( + maxTimeRemainingSeconds.current - timeRemaining, + ) + ) { + playbackStartTrackedRef.current = true + onPlaybackStart(autoplay) + } + if (!isGif && reportDialogMetadata) { + reportDialogMetadata.current.videoTimestampSeconds = Math.max( + 0, + maxTimeRemainingSeconds.current - timeRemaining, + ) + } } }} onError={e => { diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts index 8c4ef57d20..56fb66f18e 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts @@ -6,6 +6,7 @@ export type VideoEmbedInnerWebProps = { setActive: () => void onScreen: boolean lastKnownTime: React.RefObject + onPlaybackStart: (autoplay: boolean) => void } export class HLSUnsupportedError extends Error { diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx index 947d4ac3d6..213dbfa911 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -4,6 +4,7 @@ import {useLingui} from '@lingui/react/macro' import type * as HlsTypes from 'hls.js' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import {hasPlaybackStarted} from '#/lib/media/video/analytics' import {atoms as a} from '#/alf' import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog' import {useFullscreen} from '#/components/hooks/useFullscreen' @@ -29,6 +30,7 @@ export function VideoEmbedInnerWeb({ setActive, onScreen, lastKnownTime, + onPlaybackStart, }: VideoEmbedInnerWebProps) { const containerRef = useRef(null) const videoRef = useRef(null) @@ -40,6 +42,7 @@ export function VideoEmbedInnerWeb({ const [isFullscreen] = useFullscreen(containerRef) const isGif = embed.presentation === 'gif' const reportDialogMetadata = useReportDialogMetadataContext() + const playbackStartTrackedRef = useRef(false) // send error up to error boundary const [error, setError] = useState(null) @@ -79,6 +82,13 @@ export function VideoEmbedInnerWeb({ onTimeUpdate={e => { const currentTime = e.currentTarget.currentTime lastKnownTime.current = currentTime + if ( + !playbackStartTrackedRef.current && + hasPlaybackStarted(currentTime) + ) { + playbackStartTrackedRef.current = true + onPlaybackStart(!focused) + } if ( !isGif && reportDialogMetadata && diff --git a/src/components/Post/Embed/VideoEmbed/index.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx index dc394b8a6e..66ec01da9f 100644 --- a/src/components/Post/Embed/VideoEmbed/index.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.tsx @@ -23,9 +23,10 @@ import * as VideoFallback from './VideoEmbedInner/VideoFallback' interface Props { embed: app.bsky.embed.video.View + post?: app.bsky.feed.defs.PostView } -export function VideoEmbed({embed}: Props) { +export function VideoEmbed({embed, post}: Props) { const [key, setKey] = useState(0) const renderError = useCallback( @@ -52,7 +53,7 @@ export function VideoEmbed({embed}: Props) { const contents = ( - + ) @@ -69,7 +70,7 @@ export function VideoEmbed({embed}: Props) { ) } -function InnerWrapper({embed}: Props) { +function InnerWrapper({embed, post}: Props) { const {_} = useLingui() const ax = useAnalytics() const ref = useRef<{togglePlayback: () => void}>(null) @@ -86,6 +87,8 @@ function InnerWrapper({embed}: Props) { * the active position cost nothing. */ const telemetryRef = useRef(null) + const impressionTrackedRef = useRef(false) + const playbackStartTrackedRef = useRef(false) useEffect(() => { return () => { telemetryRef.current?.deactivated() @@ -121,15 +124,37 @@ function InnerWrapper({embed}: Props) { setIsActive={active => { setIsActive(active) if (active) { - telemetryRef.current ??= createPlaybackTelemetry({ - surface: 'feed', - presentation: embed.presentation === 'gif' ? 'gif' : 'video', - }) + if (!impressionTrackedRef.current) { + impressionTrackedRef.current = true + ax.metric('video:impression', { + postUri: post?.uri, + postAuthorDid: post?.author.did, + context: 'embed', + presentation: embed.presentation === 'gif' ? 'gif' : 'video', + }) + } + if (telemetryRef.current == null) { + telemetryRef.current = createPlaybackTelemetry({ + surface: 'feed', + presentation: embed.presentation === 'gif' ? 'gif' : 'video', + }) + } telemetryRef.current.activated() } else { telemetryRef.current?.deactivated() } }} + onPlaybackStart={autoplay => { + if (playbackStartTrackedRef.current) return + playbackStartTrackedRef.current = true + ax.metric('video:playback:start', { + postUri: post?.uri, + postAuthorDid: post?.author.did, + context: 'embed', + presentation: embed.presentation === 'gif' ? 'gif' : 'video', + autoplay, + }) + }} onError={error => { telemetryRef.current?.error(error) ax.metric('video:playback:failed', { diff --git a/src/components/Post/Embed/VideoEmbed/index.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx index a02fff613a..0c3f217791 100644 --- a/src/components/Post/Embed/VideoEmbed/index.web.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx @@ -37,7 +37,13 @@ const noop = () => {} */ const MIN_CARD_WIDTH = 280 -export function VideoEmbed({embed}: {embed: app.bsky.embed.video.View}) { +export function VideoEmbed({ + embed, + post, +}: { + embed: app.bsky.embed.video.View + post?: app.bsky.feed.defs.PostView +}) { const t = useTheme() const ref = useRef(null) const { @@ -47,13 +53,28 @@ export function VideoEmbed({embed}: {embed: app.bsky.embed.video.View}) { currentActiveView, } = useActiveVideoWeb() const [onScreen, setOnScreen] = useState(false) + const [meaningfullyVisible, setMeaningfullyVisible] = useState(false) const [isFullscreen] = useFullscreen() const lastKnownTime = useRef(undefined) + const impressionTrackedRef = useRef(false) + const playbackStartTrackedRef = useRef(false) + const ax = useAnalytics() const isGif = embed.presentation === 'gif' // GIFs don't participate in the "one video at a time" system const active = isGif || activeFromContext + useEffect(() => { + if (!meaningfullyVisible || impressionTrackedRef.current) return + impressionTrackedRef.current = true + ax.metric('video:impression', { + postUri: post?.uri, + postAuthorDid: post?.author.did, + context: 'embed', + presentation: isGif ? 'gif' : 'video', + }) + }, [ax, isGif, meaningfullyVisible, post?.author.did, post?.uri]) + useEffect(() => { if (!ref.current) return if (isFullscreen && !IS_WEB_FIREFOX) return @@ -62,6 +83,9 @@ export function VideoEmbed({embed}: {embed: app.bsky.embed.video.View}) { const entry = entries[0] if (!entry) return setOnScreen(entry.isIntersecting) + setMeaningfullyVisible( + entry.isIntersecting && entry.intersectionRatio >= 0.5, + ) // GIFs don't send position - they don't compete to be the active video if (!isGif) { sendPosition( @@ -179,6 +203,17 @@ export function VideoEmbed({embed}: {embed: app.bsky.embed.video.View}) { setActive={setActive} onScreen={onScreen} lastKnownTime={lastKnownTime} + onPlaybackStart={autoplay => { + if (playbackStartTrackedRef.current) return + playbackStartTrackedRef.current = true + ax.metric('video:playback:start', { + postUri: post?.uri, + postAuthorDid: post?.author.did, + context: 'embed', + presentation: isGif ? 'gif' : 'video', + autoplay, + }) + }} /> diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index 5e195ac2f9..1468f9b3f5 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -134,6 +134,7 @@ function MediaEmbed({ @@ -144,7 +145,7 @@ function MediaEmbed({ - + ) } diff --git a/src/components/Post/KnownLikers.tsx b/src/components/Post/KnownLikers.tsx new file mode 100644 index 0000000000..0f0ded9ede --- /dev/null +++ b/src/components/Post/KnownLikers.tsx @@ -0,0 +1,113 @@ +import {View} from 'react-native' +import {AtUri} from '@atproto/syntax' +import {moderateProfile} from '@bsky/sdk/moderation' +import {Trans, useLingui} from '@lingui/react/macro' + +import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {atoms as a, useTheme} from '#/alf' +import {AvatarStack} from '#/components/AvatarStack' +import {InlineLinkText, Link} from '#/components/Link' +import {ProfileHoverCard} from '#/components/ProfileHoverCard' +import {Text} from '#/components/Typography' +import {type Features, useAnalytics} from '#/analytics' +import {type app} from '#/lexicons' + +/** + * Social proof for a post. When the viewer follows some of the post's recent + * likers, renders a face pile plus "Liked by A and B". Renders nothing when + * the feature is disabled or no visible known likers are available. + */ +export function KnownLikers({ + post, + feature, + variant = 'thread', +}: { + post: app.bsky.feed.defs.PostView + feature: Features + variant?: 'feed' | 'thread' +}) { + const t = useTheme() + const {t: l} = useLingui() + const moderationOpts = useModerationOpts() + const ax = useAnalytics() + + const knownLikersAndModeration = moderationOpts + ? (post.viewer?.knownLikers?.actors ?? []) + .map(actor => ({ + actor, + moderation: moderateProfile(actor, moderationOpts), + })) + .filter(({moderation}) => !moderation.ui('profileList').filter) + : [] + + if (knownLikersAndModeration.length === 0 || !ax.features.enabled(feature)) { + return null + } + + const urip = new AtUri(post.uri) + const likesHref = makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') + const aviStackProfiles = knownLikersAndModeration + .slice(0, 3) + .map(({actor}) => actor) + const names = knownLikersAndModeration + .slice(0, 2) + .map(({actor, moderation}) => ({ + did: actor.did, + href: makeProfileLink(actor), + displayName: sanitizeDisplayName( + actor.displayName || actor.handle, + moderation.ui('displayName'), + ), + })) + const isFeed = variant === 'feed' + const rowLabel = + names.length >= 2 + ? l`Liked by ${names[0].displayName} and ${names[1].displayName}` + : l`Liked by ${names[0].displayName}` + const textStyle = [a.text_sm, t.atoms.text_contrast_medium] + const nameStyle = [a.text_sm, a.font_medium, t.atoms.text_contrast_medium] + + const nameLink = (name: (typeof names)[number]) => ( + + + {name.displayName} + + + ) + + return ( + + ax.metric('post:likedBy:click', {})}> + + + {names.length >= 2 ? ( + + Liked by {nameLink(names[0])} and {nameLink(names[1])} + + ) : ( + + Liked by {nameLink(names[0])} + + )} + + + + ) +} diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index 5cf9b0b5a4..cbe23d6724 100644 --- a/src/components/PostControls/PostMenu/PostMenuItems.tsx +++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx @@ -429,14 +429,13 @@ let PostMenuItems = ({ type: 'error', }) } - } finally { - ax.metric('postMenu:blockAccount', { - uri: postUri, - authorDid: postAuthor.did, - logContext, - feedDescriptor: feedFeedback.feedDescriptor, - }) } + ax.metric('postMenu:blockAccount', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) } const onMuteAuthor = async () => { @@ -452,14 +451,13 @@ let PostMenuItems = ({ type: 'error', }) } - } finally { - ax.metric('postMenu:unmuteAccount', { - uri: postUri, - authorDid: postAuthor.did, - logContext, - feedDescriptor: feedFeedback.feedDescriptor, - }) } + ax.metric('postMenu:unmuteAccount', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) } else { try { await queueMute() @@ -472,14 +470,13 @@ let PostMenuItems = ({ type: 'error', }) } - } finally { - ax.metric('postMenu:muteAccount', { - uri: postUri, - authorDid: postAuthor.did, - logContext, - feedDescriptor: feedFeedback.feedDescriptor, - }) } + ax.metric('postMenu:muteAccount', { + uri: postUri, + authorDid: postAuthor.did, + logContext, + feedDescriptor: feedFeedback.feedDescriptor, + }) } } diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx index 6457267780..5a85f7a9ca 100644 --- a/src/components/PostControls/index.tsx +++ b/src/components/PostControls/index.tsx @@ -107,9 +107,10 @@ let PostControls = ({ return } + const existingLike = post.viewer?.like try { setHasLikeIconBeenToggled(true) - if (!post.viewer?.like) { + if (!existingLike) { sendInteraction({ item: post.uri, event: 'app.bsky.feed.defs#interactionLike', @@ -137,8 +138,9 @@ let PostControls = ({ return } + const existingRepost = post.viewer?.repost try { - if (!post.viewer?.repost) { + if (!existingRepost) { sendInteraction({ item: post.uri, event: 'app.bsky.feed.defs#interactionRepost', diff --git a/src/components/ProfileBadges.tsx b/src/components/ProfileBadges.tsx index cb877179ac..ff8a3edaba 100644 --- a/src/components/ProfileBadges.tsx +++ b/src/components/ProfileBadges.tsx @@ -86,17 +86,20 @@ export function ProfileBadges({ const gap = isOnTheSmallSide ? a.gap_2xs : a.gap_xs const padding = gap.gap / 2 + const hitSlops = [] let visibleBadgeIndex = 0 - const hitSlops = badgeVisibility.map(isVisible => { - if (!isVisible) return HITSLOP_20 - + for (const isVisible of badgeVisibility) { + if (!isVisible) { + hitSlops.push(HITSLOP_20) + continue + } const index = visibleBadgeIndex++ - return { + hitSlops.push({ ...HITSLOP_20, left: index === 0 ? HITSLOP_20.left : padding, right: index === badgeCount - 1 ? HITSLOP_20.right : padding, - } - }) + }) + } return ( diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index d827e0010b..71653896cb 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -491,16 +491,21 @@ export function FollowButtonInner({ const onPressFollow = async (e: GestureResponderEvent) => { e.preventDefault() e.stopPropagation() + const displayNameOrHandle = profile.displayName || profile.handle try { await queueFollow() Toast.show( l`Following ${sanitizeDisplayName( - profile.displayName || profile.handle, + displayNameOrHandle, moderation.ui('displayName'), )}`, ) - onPressProp?.(e) - onFollow?.() + if (onPressProp) { + onPressProp(e) + } + if (onFollow) { + onFollow() + } } catch (e) { const err = e as Error if (err?.name !== 'AbortError') { @@ -514,15 +519,18 @@ export function FollowButtonInner({ const onPressUnfollow = async (e: GestureResponderEvent) => { e.preventDefault() e.stopPropagation() + const displayNameOrHandle = profile.displayName || profile.handle try { await queueUnfollow() Toast.show( l`No longer following ${sanitizeDisplayName( - profile.displayName || profile.handle, + displayNameOrHandle, moderation.ui('displayName'), )}`, ) - onPressProp?.(e) + if (onPressProp) { + onPressProp(e) + } } catch (e) { const err = e as Error if (err?.name !== 'AbortError') { diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index 1ab2668ae5..5e6a3a97ca 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -270,10 +270,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { // Track seen profiles const seenProfilesRef = useRef>(new Set()) - const itemsRef = useRef(items) - itemsRef.current = items - const selectedInterestRef = useRef(selectedInterest) - selectedInterestRef.current = selectedInterest const onViewableItemsChanged = useNonReactiveCallback( ({viewableItems}: {viewableItems: ViewToken[]}) => { @@ -282,7 +278,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { if (item.type === 'profile') { if (!seenProfilesRef.current.has(item.profile.did)) { seenProfilesRef.current.add(item.profile.did) - const position = itemsRef.current.findIndex( + const position = items.findIndex( i => i.type === 'profile' && i.profile.did === item.profile.did, ) ax.metric('suggestedUser:seen', { @@ -292,9 +288,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { position: position !== -1 ? position : 0, suggestedDid: item.profile.did, category: - selectedInterestRef.current === FOR_YOU_TAB - ? null - : selectedInterestRef.current, + selectedInterest === FOR_YOU_TAB ? null : selectedInterest, }) } } diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 1fa020f2a9..e3b7090fa7 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -2,6 +2,7 @@ import {useMemo} from 'react' import {type StyleProp, type TextStyle} from 'react-native' import {RichText as RichTextAPI} from '@bsky/sdk/richtext' +import {isRTLText} from '#/lib/strings/text-direction' import {toShortUrl} from '#/lib/strings/url-helpers' import {android, atoms as a, flatten, type TextStyleProp} from '#/alf' import {isOnlyEmoji} from '#/alf/typography' @@ -9,6 +10,7 @@ import {InlineLinkText, type LinkProps} from '#/components/Link' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichTextTag} from '#/components/RichTextTag' import {Text, type TextProps} from '#/components/Typography' +import {IS_NATIVE} from '#/env' import {app} from '#/lexicons' import * as bsky from '#/types/bsky' @@ -82,15 +84,17 @@ export function RichText({ } }, [value]) - const plainStyles = style + const {text, facets} = richText + const plainStyles: StyleProp = [ + style, + IS_NATIVE && isRTLText(text) ? {textAlign: 'right'} : null, + ] const suffixStyles = suffix && suffixOffset ? android({paddingBottom: suffixOffset, marginBottom: -suffixOffset}) : null const interactiveStyles = [plainStyles, interactiveStyle] - const {text, facets} = richText - if (!facets?.length) { if (isOnlyEmoji(text)) { const flattenedStyle = flatten(style) diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index af5849346b..f8b01b5afa 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -122,9 +122,12 @@ export function Trigger({children, hitSlop, label}: TriggerProps) { } } +const defaultValueText: NonNullable = value => + value.label + export function ValueText({ placeholder, - children = value => value.label, + children = defaultValueText, style, }: ValueProps) { const [value] = useContext(ValueTextContext) diff --git a/src/components/StarterPack/Main/ProfilesList.tsx b/src/components/StarterPack/Main/ProfilesList.tsx index 56ac697a31..05307be47a 100644 --- a/src/components/StarterPack/Main/ProfilesList.tsx +++ b/src/components/StarterPack/Main/ProfilesList.tsx @@ -2,22 +2,31 @@ import {forwardRef, useCallback, useImperativeHandle, useState} from 'react' import {type ListRenderItemInfo, View} from 'react-native' import {AtUri} from '@atproto/syntax' import {type ModerationOpts} from '@bsky/sdk/moderation' +import {useLingui} from '@lingui/react/macro' +import {Trans} from '@lingui/react/macro' import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' +import {cleanError} from '#/lib/strings/errors' import {useAllListMembersQuery} from '#/state/queries/list-members' +import {useListMembershipRemoveMutation} from '#/state/queries/list-memberships' import {useSession} from '#/state/session' import {List, type ListRef} from '#/view/com/util/List' import {type SectionRef} from '#/screens/Profile/Sections/types' import {atoms as a, useTheme} from '#/alf' +import * as Admonition from '#/components/Admonition' +import {ButtonIcon, ButtonText} from '#/components/Button' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' +import {Loader} from '#/components/Loader' import {Default as ProfileCard} from '#/components/ProfileCard' +import * as Toast from '#/components/Toast' +import {useAnalytics} from '#/analytics' import {IS_NATIVE, IS_WEB} from '#/env' import {type app} from '#/lexicons' -function keyExtractor(item: app.bsky.actor.defs.ProfileView, index: number) { - return `${item.did}-${index}` +function keyExtractor(item: app.bsky.graph.defs.ListItemView) { + return item.uri } interface ProfilesListProps { @@ -42,26 +51,26 @@ export const ProfilesList = forwardRef( // The server returns these sorted by descending creation date, so we want to invert - const profiles = data + const listItems = data ?.filter( p => !isBlockedOrBlocking(p.subject) && !p.subject.associated?.labeler, ) - .map(p => p.subject) .reverse() const isOwn = new AtUri(listUri).host === currentAccount?.did const getSortedProfiles = () => { - if (!profiles) return - if (!isOwn) return profiles + if (!listItems) return - const myIndex = profiles.findIndex(p => p.did === currentAccount?.did) - return myIndex !== -1 - ? [ - profiles[myIndex], - ...profiles.slice(0, myIndex), - ...profiles.slice(myIndex + 1), - ] - : profiles + return [...listItems].sort((a, b) => { + if (a.subjectOptedOut !== b.subjectOptedOut) { + return a.subjectOptedOut ? -1 : 1 + } + if (isOwn) { + if (a.subject.did === currentAccount?.did) return -1 + if (b.subject.did === currentAccount?.did) return 1 + } + return 0 + }) } const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({ @@ -77,7 +86,7 @@ export const ProfilesList = forwardRef( const renderItem = ({ item, index, - }: ListRenderItemInfo) => { + }: ListRenderItemInfo) => { return ( ( (IS_WEB || index !== 0) && a.border_t, ]}> + {item.subjectOptedOut ? ( + + ) : null} ) } + const onRefresh = async () => { + setIsPTRing(true) + await refetch() + setIsPTRing(false) + } + if (!data) { return ( ( desktopFixedHeight initialNumToRender={initialNumToRender} refreshing={isPTRing} - onRefresh={async () => { - setIsPTRing(true) - await refetch() - setIsPTRing(false) - }} + onRefresh={() => void onRefresh()} /> ) }, ) + +function OptedOutControls({ + item, + listUri, + canRemove, +}: { + item: app.bsky.graph.defs.ListItemView + listUri: string + canRemove: boolean +}) { + const {t: l} = useLingui() + const ax = useAnalytics() + const [isRemoved, setIsRemoved] = useState(false) + const {mutate: removeMembership, isPending} = useListMembershipRemoveMutation( + { + onSuccess: () => { + setIsRemoved(true) + Toast.show(l`Removed from Starter Pack`) + }, + onError: error => + Toast.show(cleanError(error), { + type: 'error', + }), + }, + ) + + if (isRemoved) return null + + return ( + + + + + + Opted out of this Starter Pack + + + {canRemove ? ( + { + ax.metric('starterPack:removeUser', {context: 'opt-out'}) + removeMembership({ + listUri, + actorDid: item.subject.did, + membershipUri: item.uri, + }) + }}> + {isPending ? ( + + ) : ( + + Remove + + )} + + ) : null} + + + ) +} diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index 4555c6ec4b..67e600fe3f 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -6,9 +6,7 @@ import { View, type ViewStyle, } from 'react-native' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack' @@ -54,7 +52,7 @@ interface ProfileFeedgensProps { isMe: boolean emptyStateMessage?: string emptyStateButton?: EmptyStateButtonProps - emptyStateIcon?: React.ComponentType | React.ReactElement + emptyStateIcon?: React.ComponentType | React.ReactElement } function keyExtractor(item: app.bsky.graph.defs.StarterPackViewBasic) { @@ -90,7 +88,7 @@ export function ProfileStarterPacks({ const {isTabletOrDesktop} = useWebMediaQueries() const items = data?.pages.flatMap(page => page.starterPacks) - const {_} = useLingui() + const {t: l} = useLingui() const EmptyComponent = useCallback(() => { if (emptyStateMessage || emptyStateButton || emptyStateIcon) { @@ -101,9 +99,7 @@ export function ProfileStarterPacks({ iconSize="3xl" message={ emptyStateMessage ?? - _( - msg`Starter packs let you share your favorite feeds and people with your friends.`, - ) + l`Starter Packs let you share your favorite feeds and people with your friends.` } button={emptyStateButton} /> @@ -111,7 +107,7 @@ export function ProfileStarterPacks({ ) } return - }, [_, emptyStateMessage, emptyStateButton, emptyStateIcon]) + }, [l, emptyStateMessage, emptyStateButton, emptyStateIcon]) useImperativeHandle(ref, () => ({ scrollToTop: () => {}, @@ -122,7 +118,7 @@ export function ProfileStarterPacks({ try { await refetch() } catch (err) { - logger.error('Failed to refresh starter packs', {message: err}) + logger.error('Failed to refresh Starter Packs', {message: err}) } setIsPTRing(false) }, [refetch, setIsPTRing]) @@ -132,7 +128,7 @@ export function ProfileStarterPacks({ try { await fetchNextPage() } catch (err) { - logger.error('Failed to load more starter packs', {message: err}) + logger.error('Failed to load more Starter Packs', {message: err}) } }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) @@ -179,8 +175,8 @@ export function ProfileStarterPacks({ }} removeClippedSubviews={true} desktopFixedHeight - onEndReached={onEndReached} - onRefresh={onRefresh} + onEndReached={() => void onEndReached()} + onRefresh={() => void onRefresh()} ListEmptyComponent={ data ? (isMe ? EmptyComponent : undefined) : FeedLoadingPlaceholder } @@ -193,7 +189,7 @@ export function ProfileStarterPacks({ } function CreateAnother() { - const {_} = useLingui() + const {t: l} = useLingui() const t = useTheme() const navigation = useNavigation() @@ -207,7 +203,7 @@ function CreateAnother() { t.atoms.border_contrast_low, ]}> - - Generate a starter pack + Generate a Starter Pack @@ -349,12 +344,12 @@ function Empty() { { navigation.navigate('StarterPackWizard', {}) }} @@ -363,21 +358,17 @@ function Empty() { {}} showCancel={false} /> ) diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx index 5ff6213825..95c2a51223 100644 --- a/src/components/StarterPack/QrCodeDialog.tsx +++ b/src/components/StarterPack/QrCodeDialog.tsx @@ -164,7 +164,7 @@ export function QrCodeDialog({ + label={_(msg`Create a QR code for a Starter Pack`)}> }> {!link ? ( diff --git a/src/components/StarterPack/ShareDialog.tsx b/src/components/StarterPack/ShareDialog.tsx index 17c794fc1d..8db64dfb29 100644 --- a/src/components/StarterPack/ShareDialog.tsx +++ b/src/components/StarterPack/ShareDialog.tsx @@ -53,14 +53,15 @@ function ShareDialogInner({ const imageUrl = getStarterPackOgCard(starterPack) - const onShareLink = async () => { + const onShareLink = () => { if (!link) return - shareUrl(link) ax.metric('starterPack:share', { starterPack: starterPack.uri, shareType: 'link', }) - control.close() + control.close(() => { + void shareUrl(link) + }) } const saveImageToAlbum = useSaveImageToMediaLibrary() @@ -80,11 +81,11 @@ function ShareDialogInner({ - Invite people to this starter pack! + Invite people to this Starter Pack! - Share this starter pack and help people join your community on + Share this Starter Pack and help people join your community on Bluesky. diff --git a/src/components/StarterPack/StarterPackCard.tsx b/src/components/StarterPack/StarterPackCard.tsx index 2f99ab0651..b8d582902a 100644 --- a/src/components/StarterPack/StarterPackCard.tsx +++ b/src/components/StarterPack/StarterPackCard.tsx @@ -2,9 +2,7 @@ import {useMemo} from 'react' import {View} from 'react-native' import {Image} from 'expo-image' import {AtUri} from '@atproto/syntax' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Plural, Trans} from '@lingui/react/macro' +import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {sanitizeHandle} from '#/lib/strings/handles' @@ -59,9 +57,10 @@ export function Card({ }) { const {record, creator, joinedAllTimeCount} = starterPack - const {_} = useLingui() + const {t: l} = useLingui() const t = useTheme() const {currentAccount} = useSession() + const isOwnStarterPack = creator?.did === currentAccount?.did if (!bsky.isType(app.bsky.graph.starterpack, record)) { return null @@ -82,9 +81,9 @@ export function Card({ emoji style={[a.leading_snug, t.atoms.text_contrast_medium]} numberOfLines={1}> - {creator?.did === currentAccount?.did - ? _(msg`Starter pack by you`) - : _(msg`Starter pack by ${sanitizeHandle(creator.handle, '@')}`)} + {isOwnStarterPack + ? l`Starter Pack by you` + : l`Starter Pack by ${sanitizeHandle(creator.handle, '@')}`} @@ -95,7 +94,7 @@ export function Card({ ) : null} {!!joinedAllTimeCount && joinedAllTimeCount >= 50 && ( - + joined! @@ -109,7 +108,7 @@ export function useStarterPackLink({ }: { view: bsky.starterPack.AnyStarterPackView }) { - const {_} = useLingui() + const {t: l} = useLingui() const qc = useQueryClient() const {rkey, handleOrDid} = useMemo(() => { const rkey = new AtUri(view.uri).rkey @@ -124,8 +123,8 @@ export function useStarterPackLink({ return { to: `/starter-pack/${handleOrDid}/${rkey}`, label: bsky.isType(app.bsky.graph.starterpack, view.record) - ? _(msg`Navigate to ${view.record.name}`) - : _(msg`Navigate to starter pack`), + ? l`Navigate to ${view.record.name}` + : l`Navigate to Starter Pack`, precache, } } @@ -138,7 +137,7 @@ export function Link({ onPress?: () => void children: BaseLinkProps['children'] }) { - const {_} = useLingui() + const {t: l} = useLingui() const queryClient = useQueryClient() const {record} = starterPack const {rkey, handleOrDid} = useMemo(() => { @@ -154,7 +153,7 @@ export function Link({ return ( { precacheResolvedUri( queryClient, diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx index 39397f38e3..3c2c52e876 100644 --- a/src/components/StarterPack/Wizard/WizardListCard.tsx +++ b/src/components/StarterPack/Wizard/WizardListCard.tsx @@ -5,9 +5,7 @@ import { type ModerationOpts, type ModerationUI, } from '@bsky/sdk/moderation' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants' import {sanitizeDisplayName} from '#/lib/strings/display-names' @@ -36,6 +34,7 @@ function WizardListCard({ avatar, included, disabled, + subjectOptedOut, moderationUi, }: { type: 'user' | 'algo' @@ -48,18 +47,19 @@ function WizardListCard({ avatar?: string included?: boolean disabled?: boolean + subjectOptedOut?: boolean moderationUi: ModerationUI }) { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() return ( {subtitle} + {subjectOptedOut ? ( + + Opted out + + ) : null} {btnType === 'checkbox' ? ( ) : !disabled ? ( @@ -396,7 +396,7 @@ export function AppClipOverlay({ Download Bluesky to get started! - We'll remember the starter pack you chose and use it when you create + We’ll remember the Starter Pack you chose and use it when you create an account in the app. diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index e73bab09ce..8ff2b76591 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -4,9 +4,7 @@ import {Image} from 'expo-image' import {AtUri} from '@atproto/syntax' import {type ModerationOpts} from '@bsky/sdk/moderation' import {RichText as RichTextAPI} from '@bsky/sdk/richtext' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Plural, Trans} from '@lingui/react/macro' +import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {useQueryClient} from '@tanstack/react-query' @@ -30,6 +28,7 @@ import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useShortenLink} from '#/state/queries/shorten-link' import { useDeleteStarterPackMutation, + useReferenceListOptOutMutation, useStarterPackQuery, } from '#/state/queries/starter-packs' import {useAppviewClient, usePdsClient, useSession} from '#/state/session' @@ -95,7 +94,7 @@ export function StarterPackScreen({route}: StarterPackScreeProps) { } export function StarterPackScreenShort({route}: StarterPackScreenShortProps) { - const {_} = useLingui() + const {t: l} = useLingui() const { data: resolvedStarterPack, isLoading, @@ -110,8 +109,8 @@ export function StarterPackScreenShort({route}: StarterPackScreenShortProps) { ) @@ -129,7 +128,7 @@ export function StarterPackScreenInner({ routeParams: StarterPackScreeProps['route']['params'] }) { const {name, rkey} = routeParams - const {_} = useLingui() + const {t: l} = useLingui() const {currentAccount} = useSession() const moderationOpts = useModerationOpts() @@ -155,8 +154,8 @@ export function StarterPackScreenInner({ ) } @@ -186,13 +185,13 @@ function StarterPackScreenLoaded({ const showPeopleTab = Boolean(starterPack.list) const showFeedsTab = Boolean(starterPack.feeds?.length) const showPostsTab = Boolean(starterPack.list) - const {_} = useLingui() + const {t: l} = useLingui() const ax = useAnalytics() const tabs = [ - ...(showPeopleTab ? [_(msg`People`)] : []), - ...(showFeedsTab ? [_(msg`Feeds`)] : []), - ...(showPostsTab ? [_(msg`Posts`)] : []), + ...(showPeopleTab ? [l`People`] : []), + ...(showFeedsTab ? [l`Feeds`] : []), + ...(showPostsTab ? [l`Posts`] : []), ] const qrCodeDialogControl = useDialogControl() @@ -210,7 +209,7 @@ function StarterPackScreenLoaded({ const onOpenShareDialog = useCallback(() => { const rkey = new AtUri(starterPack.uri).rkey - shortenLink(makeStarterPackLink(starterPack.creator.did, rkey)).then( + void shortenLink(makeStarterPackLink(starterPack.creator.did, rkey)).then( res => { setLink(res.url) }, @@ -300,7 +299,7 @@ function Header({ routeParams: StarterPackScreeProps['route']['params'] onOpenShareDialog: () => void }) { - const {_} = useLingui() + const {t: l} = useLingui() const t = useTheme() const {currentAccount, hasSession} = useSession() const appviewClient = useAppviewClient() @@ -350,10 +349,10 @@ function Header({ listItems = await getAllListMembers(appviewClient, starterPack.list.uri) } catch (e) { setIsProcessing(false) - Toast.show(_(msg`An error occurred while trying to follow all`), { + Toast.show(l`An error occurred while trying to follow all`, { type: 'error', }) - logger.error('Failed to get list members for starter pack', { + logger.error('Failed to get list members for Starter Pack', { safeMessage: e, }) return @@ -377,7 +376,7 @@ function Header({ }) } catch (e) { setIsProcessing(false) - Toast.show(_(msg`An error occurred while trying to follow all`), { + Toast.show(l`An error occurred while trying to follow all`, { type: 'error', }) logger.error('Failed to follow all accounts', {safeMessage: e}) @@ -391,7 +390,7 @@ function Header({ }) } }) - Toast.show(_(msg`All accounts have been followed!`)) + Toast.show(l`All accounts have been followed!`) captureAction(ProgressGuideAction.Follow, dids.length) ax.metric('starterPack:followAll', { logContext: 'StarterPackProfilesList', @@ -426,7 +425,7 @@ function Header({ {isOwn ? ( + + + + ) : null} () const {gtMobile} = useBreakpoints() @@ -739,8 +827,8 @@ function InvalidStarterPack({rkey}: {rkey: string}) { }, onError: e => { setIsProcessing(false) - logger.error('Failed to delete invalid starter pack', {safeMessage: e}) - Toast.show(_(msg`Failed to delete starter pack`), { + logger.error('Failed to delete invalid Starter Pack', {safeMessage: e}) + Toast.show(l`Failed to delete Starter Pack`, { type: 'error', }) }, @@ -751,7 +839,7 @@ function InvalidStarterPack({rkey}: {rkey: string}) { - Starter pack is invalid + Starter Pack is invalid - The starter pack that you are trying to view is invalid. You may - delete this starter pack instead. + The Starter Pack that you are trying to view is invalid. You may + delete this Starter Pack instead. @@ -771,7 +859,7 @@ function InvalidStarterPack({rkey}: {rkey: string}) { - - @@ -122,17 +118,17 @@ export function DraftsButton({ {canSaveDraft && ( void handleSaveAndOpen()} color="primary" /> )} + - diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx index 9471dc0d2e..a8e7076a48 100644 --- a/src/view/com/composer/labels/LabelsBtn.tsx +++ b/src/view/com/composer/labels/LabelsBtn.tsx @@ -101,7 +101,7 @@ function DialogInner({ return ( + style={[a.w_full, web({maxWidth: 500})]}> diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index ec02daa7d7..085233ff11 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -1,4 +1,3 @@ -import {useCallback} from 'react' import * as MediaLibrary from 'expo-media-library/legacy' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -20,13 +19,23 @@ export function OpenCameraBtn({disabled, onAdd}: OpenCameraBtnProps) { MediaLibrary.usePermissions({granularPermissions: ['photo']}) const t = useTheme() - const onPressTakePicture = useCallback(async () => { + const mediaGranted = mediaPermissionRes?.granted + const mediaCanAskAgain = mediaPermissionRes?.canAskAgain + + /* + * No useCallback: with the diagnostics above resolved this component compiles, + * so React Compiler memoizes it, and the hand-written deps were what it could + * not preserve. + */ + const onPressTakePicture = async () => { try { if (!(await requestCameraAccessIfNeeded())) { return } - if (!mediaPermissionRes?.granted && mediaPermissionRes?.canAskAgain) { - await requestMediaPermission() + if (!mediaGranted) { + if (mediaCanAskAgain) { + await requestMediaPermission() + } } const img = await openCamera({ @@ -49,12 +58,7 @@ export function OpenCameraBtn({disabled, onAdd}: OpenCameraBtnProps) { // ignore logger.warn('Error using camera', {error: err}) } - }, [ - onAdd, - requestCameraAccessIfNeeded, - mediaPermissionRes, - requestMediaPermission, - ]) + } const shouldShowCameraButton = IS_NATIVE || IS_WEB_MOBILE if (!shouldShowCameraButton) { diff --git a/src/view/com/composer/state/composer.test.ts b/src/view/com/composer/state/composer.test.ts new file mode 100644 index 0000000000..fc91e4b913 --- /dev/null +++ b/src/view/com/composer/state/composer.test.ts @@ -0,0 +1,60 @@ +import {composerReducer, createComposerState} from './composer' + +jest.mock('#/state/gallery', () => ({ + createInitialImages: jest.fn(), +})) +jest.mock('#/logger', () => ({ + logger: { + warn: jest.fn(), + }, +})) +jest.mock('#/state/queries/postgate/util', () => ({ + createPostgateRecord: jest.fn(() => ({})), +})) +jest.mock('#/state/queries/threadgate', () => ({ + threadgateRecordToAllowUISetting: jest.fn(() => []), +})) + +function createState() { + return createComposerState({ + initText: undefined, + initMention: undefined, + initImageUris: undefined, + initQuoteUri: undefined, + initInteractionSettings: undefined, + }) +} + +describe('composerReducer', () => { + describe('add_post', () => { + it('selects the appended post and requests focus', () => { + const state = createState() + + const nextState = composerReducer(state, {type: 'add_post'}) + + expect(nextState.thread.posts).toHaveLength(2) + expect(nextState.activePostIndex).toBe(1) + expect(nextState.mutableNeedsFocusActive).toBe(true) + }) + + it('selects a post inserted in the middle of a thread', () => { + let state = createState() + state = composerReducer(state, {type: 'add_post'}) + state = composerReducer(state, {type: 'add_post'}) + + const lastPostId = state.thread.posts[2].id + state = composerReducer(state, { + type: 'focus_post', + postId: state.thread.posts[0].id, + }) + + const nextState = composerReducer(state, {type: 'add_post'}) + + expect(nextState.thread.posts).toHaveLength(4) + expect(nextState.activePostIndex).toBe(1) + expect(nextState.thread.posts[2].id).not.toBe(lastPostId) + expect(nextState.thread.posts[3].id).toBe(lastPostId) + expect(nextState.mutableNeedsFocusActive).toBe(true) + }) + }) +}) diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 5a013a830b..5e56bab21f 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -253,6 +253,8 @@ export function composerReducer( return { ...state, isDirty: true, + activePostIndex: activePostIndex + 1, + mutableNeedsFocusActive: true, thread: { ...state.thread, posts: nextPosts, diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 6140fc046e..21185327a0 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -186,12 +186,10 @@ export function TextInput({ }, [t, fonts]) const textDecorated = useMemo(() => { - let i = 0 - - return Array.from(richtext.segments()).map(segment => { + return Array.from(richtext.segments()).map((segment, i) => { return ( {}) - void queryClient.resetQueries({queryKey: [RQKEY]}) + for (const [, thumbnail] of queryClient.getQueriesData( + {queryKey: [RQKEY]}, + )) { + if (thumbnail) { + deleteThumbnail(thumbnail) + } + } + queryClient.removeQueries({queryKey: [RQKEY]}) +} + +function deleteThumbnail(thumbnail: VideoThumbnailsResult) { + try { + new File(thumbnail.uri).delete() + } catch {} } export function VideoTranscodeBackdrop({uri}: {uri: string}) { const {data: thumbnail} = useQuery({ queryKey: [RQKEY, uri], - queryFn: async () => { - return await createVideoThumbnail(uri) + queryFn: async ({signal}) => { + const result = await getThumbnailAsync(uri) + if (signal.aborted) { + deleteThumbnail(result) + throw new Error('Thumbnail generation canceled') + } + return result }, }) @@ -25,7 +46,7 @@ export function VideoTranscodeBackdrop({uri}: {uri: string}) { { if (typeof file !== 'string') throw new Error( 'getVideoMetadata was passed a File, when on native it should be a uri', ) - const metadata = await getVideoMetaData(file) + const metadata = await probe(file) return { uri: file, - mimeType: extToMime(metadata.extension), + mimeType: getMimeTypeFromUri(file) ?? fallbackMimeType ?? metadata.mimeType, + fileSize: metadata.fileSize, width: metadata.width, height: metadata.height, /* - * react-native-compressor reports seconds; the rest of the app treats - * `ImagePickerAsset.duration` as milliseconds (matching expo-image-picker). + * The probe reports seconds; `ImagePickerAsset.duration` uses milliseconds. */ duration: metadata.duration * 1000, } } +function getMimeTypeFromUri(uri: string): string | undefined { + const extension = uri.match(/\.([^.?#/]+)(?:[?#]|$)/)?.[1] + if (!extension) return + + try { + return extToMime(extension) + } catch { + return + } +} + export function hasWebCodecs(): boolean { return false } diff --git a/src/view/com/composer/videos/metadata.web.ts b/src/view/com/composer/videos/metadata.web.ts index ed87d157c9..47cf3f9ea1 100644 --- a/src/view/com/composer/videos/metadata.web.ts +++ b/src/view/com/composer/videos/metadata.web.ts @@ -11,6 +11,7 @@ export function hasWebCodecs(): boolean { export async function getVideoMetadata( file: File | string, + _fallbackMimeType?: string, ): Promise { if (typeof file === 'string') throw new Error( diff --git a/src/view/com/feeds/ComposerPrompt.tsx b/src/view/com/feeds/ComposerPrompt.tsx index db42ead219..22f55e8631 100644 --- a/src/view/com/feeds/ComposerPrompt.tsx +++ b/src/view/com/feeds/ComposerPrompt.tsx @@ -56,8 +56,10 @@ export function ComposerPrompt() { requestVideoAccessIfNeeded(), ]) - if (!photoAccess && !videoAccess) { - return + if (!photoAccess) { + if (!videoAccess) { + return + } } if (Keyboard.isVisible()) { @@ -108,8 +110,10 @@ export function ComposerPrompt() { return } - if (IS_NATIVE && Keyboard.isVisible()) { - Keyboard.dismiss() + if (IS_NATIVE) { + if (Keyboard.isVisible()) { + Keyboard.dismiss() + } } const image = await openCamera({ @@ -127,8 +131,16 @@ export function ComposerPrompt() { }, ] + /* + * Statement form rather than a ternary: React Compiler cannot lower a + * conditional expression inside a `try`, and `imageUris` is built here. + */ + let nativeImageUris + if (IS_NATIVE) { + nativeImageUris = imageUris + } openComposer({ - imageUris: IS_NATIVE ? imageUris : undefined, + imageUris: nativeImageUris, logContext: 'Fab', }) } catch (err: any) { diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index effa9ad430..2082f76961 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -79,7 +79,7 @@ export function ProfileFeedgens({ error, refetch, } = useProfileFeedgensQuery(did, opts) - const isEmpty = !isPending && !data?.pages[0]?.feeds.length + const isEmpty = !isPending && !data?.pages.some(page => page.feeds.length) const {data: preferences} = usePreferencesQuery() const navigation = useNavigation() const {currentAccount} = useSession() diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx index a2f11aa9c5..c8174e4337 100644 --- a/src/view/com/home/HomeHeader.tsx +++ b/src/view/com/home/HomeHeader.tsx @@ -1,7 +1,10 @@ import {useCallback, useMemo} from 'react' +import {useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' +import {TIMELINE_SAVED_FEED} from '#/lib/constants' import {type NavigationProp} from '#/lib/routes/types' +import {getLocalizedFeedName} from '#/lib/strings/feed-names' import {type FeedSourceInfo} from '#/state/queries/feed' import {useSession} from '#/state/session' import {type RenderTabBarFnProps} from '#/view/com/pager/Pager' @@ -12,28 +15,29 @@ export function HomeHeader( props: RenderTabBarFnProps & { testID?: string onPressSelected: () => void - feeds: FeedSourceInfo[] + feeds: Pick[] }, ) { const {feeds, onSelect: onSelectProp} = props const {hasSession} = useSession() + const {t: l, i18n} = useLingui() const navigation = useNavigation() const hasPinnedCustom = useMemo(() => { if (!hasSession) return false return feeds.some(tab => { - const isFollowing = tab.uri === 'following' + const isFollowing = tab.uri === TIMELINE_SAVED_FEED.value return !isFollowing }) }, [feeds, hasSession]) const items = useMemo(() => { - const pinnedNames = feeds.map(f => f.displayName) + const pinnedNames = feeds.map(f => getLocalizedFeedName(f, i18n)) if (!hasPinnedCustom) { - return pinnedNames.concat('Feeds ✨') + return pinnedNames.concat(l`Feeds ✨`) } return pinnedNames - }, [hasPinnedCustom, feeds]) + }, [i18n, l, hasPinnedCustom, feeds]) const onPressFeedsLink = useCallback(() => { navigation.navigate('Feeds') diff --git a/src/view/com/lists/ListMembers.tsx b/src/view/com/lists/ListMembers.tsx index 2f24dfec09..a7327700c6 100644 --- a/src/view/com/lists/ListMembers.tsx +++ b/src/view/com/lists/ListMembers.tsx @@ -90,7 +90,7 @@ export function ListMembers({ hasNextPage, isFetchingNextPage, } = useListMembersQuery(list) - const isEmpty = !isFetching && !data?.pages[0].items.length + const isEmpty = !isFetching && !data?.pages.some(page => page.items.length) const isOwner = currentAccount && data?.pages[0].list.creator.did === currentAccount.did diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index afc4c63791..373cb60427 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -79,7 +79,7 @@ export function ProfileLists({ error, refetch, } = useProfileListsQuery(did, opts) - const isEmpty = !isPending && !data?.pages[0]?.lists.length + const isEmpty = !isPending && !data?.pages.some(page => page.lists.length) const {data: preferences} = usePreferencesQuery() const navigation = useNavigation() const {currentAccount} = useSession() diff --git a/src/view/com/notifications/NotificationFeed.tsx b/src/view/com/notifications/NotificationFeed.tsx index 82b5008078..a6c60a72ec 100644 --- a/src/view/com/notifications/NotificationFeed.tsx +++ b/src/view/com/notifications/NotificationFeed.tsx @@ -103,9 +103,8 @@ export function NotificationFeed({ logger.error('Failed to refresh notifications feed', { message: err, }) - } finally { - setIsPTRing(false) } + setIsPTRing(false) }, [refreshNotifications, setIsPTRing]) const onEndReached = useCallback(async () => { diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 0d56dfb873..fae4687546 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -338,7 +338,7 @@ let NotificationFeedItem = ({ * see `src/state/queries/notifications/util.ts` */ a11yLabel = starterPackName - ? l`${firstAuthorName} followed you back via starter pack ${starterPackName}` + ? l`${firstAuthorName} followed you back via Starter Pack ${starterPackName}` : l`${firstAuthorName} followed you back` notificationContent = {firstAuthorLink} followed you back } else { @@ -347,8 +347,8 @@ let NotificationFeedItem = ({ ? l`${firstAuthorName} and ${plural(additionalAuthorsCount, { one: `${formattedAuthorsCount} other`, other: `${formattedAuthorsCount} others`, - })} followed you via starter pack ${starterPackName}` - : l`${firstAuthorName} followed you via starter pack ${starterPackName}` + })} followed you via Starter Pack ${starterPackName}` + : l`${firstAuthorName} followed you via Starter Pack ${starterPackName}` : hasMultipleAuthors ? l`${firstAuthorName} and ${plural(additionalAuthorsCount, { one: `${formattedAuthorsCount} other`, @@ -407,8 +407,8 @@ let NotificationFeedItem = ({ ? l`${firstAuthorName} and ${plural(additionalAuthorsCount, { one: `${formattedAuthorsCount} other`, other: `${formattedAuthorsCount} others`, - })} signed up with your starter pack` - : l`${firstAuthorName} signed up with your starter pack` + })} signed up with your Starter Pack` + : l`${firstAuthorName} signed up with your Starter Pack` notificationContent = hasMultipleAuthors ? ( {firstAuthorLink} and{' '} @@ -419,10 +419,10 @@ let NotificationFeedItem = ({ other={`${formattedAuthorsCount} others`} /> {' '} - signed up with your starter pack + signed up with your Starter Pack ) : ( - {firstAuthorLink} signed up with your starter pack + {firstAuthorLink} signed up with your Starter Pack ) icon = ( @@ -735,8 +735,8 @@ function FollowedViaStarterPack({ return ( - - via starter pack{' '} + + via Starter Pack{' '} ('idle') - useImperativeHandle(ref, () => ({ - setPage: (index: number) => { - onTabBarSelect(index) - }, - })) - const onTabBarSelect = useCallback( (index: number) => { const scrollY = window.scrollY @@ -104,6 +98,12 @@ export function Pager({ [selectedPage, setSelectedPage, onPageSelected, onTabPressed], ) + useImperativeHandle(ref, () => ({ + setPage: (index: number) => { + onTabBarSelect(index) + }, + })) + return ( {renderTabBar({ diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 0e259544ce..4407de691c 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -25,6 +25,7 @@ export interface TabBarProps { testID?: string selectedPage: number items: string[] + align?: 'center' | 'left' onSelect?: (index: number) => void onPressSelected?: (index: number) => void dragProgress: SharedValue @@ -42,6 +43,7 @@ export function TabBar({ testID, selectedPage, items, + align = 'center', onSelect, onPressSelected, dragProgress, @@ -77,10 +79,13 @@ export function TabBar({ 'worklet' const layout = layouts.get()[index] const availableSize = containerSize.get() - 2 * CONTENT_PADDING + const maxOffset = Math.max(0, contentSize.get() - availableSize) if (!layout) { // Should not happen, but fall back to equal sizes. - const offsetPerPage = contentSize.get() - availableSize - return (index / (itemsLength - 1)) * offsetPerPage + return itemsLength > 1 ? (index / (itemsLength - 1)) * maxOffset : 0 + } + if (maxOffset === 0) { + return 0 } const freeSpace = availableSize - layout.width const accumulatingOffset = interpolate( @@ -91,7 +96,7 @@ export function TabBar({ [0, freeSpace], 'clamp', ) - return layout.x - accumulatingOffset + return Math.min(Math.max(layout.x - accumulatingOffset, 0), maxOffset) }, [itemsLength, contentSize, containerSize, layouts], ) @@ -332,13 +337,13 @@ export function TabBar({ syncScrollState.set('unsynced') }} onScroll={e => { - scrollX.value = Math.round(e.nativeEvent.contentOffset.x) + scrollX.set(Math.round(e.nativeEvent.contentOffset.x)) }}> { contentSize.set(e.nativeEvent.layout.width) }} - style={{flexDirection: 'row', flexGrow: 1}}> + style={[styles.items, align === 'center' && styles.itemsCentered]}> {items.map((item, i) => { return ( ) })} @@ -382,6 +388,7 @@ function TabBarItem({ onPressItem, onItemLayout, onTextLayout, + align, }: { index: number testID: string | undefined @@ -390,6 +397,7 @@ function TabBarItem({ onPressItem: (index: number) => void onItemLayout: (index: number, layout: {x: number; width: number}) => void onTextLayout: (index: number, layout: {width: number}) => void + align: 'center' | 'left' }) { const t = useTheme() const style = useAnimatedStyle(() => { @@ -421,10 +429,15 @@ function TabBarItem({ ) return ( - + onPressItem(index)} accessibilityRole="tab"> @@ -448,12 +461,26 @@ const styles = StyleSheet.create({ backgroundColor: 'transparent', paddingHorizontal: CONTENT_PADDING, }, - item: { + items: { + flexDirection: 'row', + }, + itemsCentered: { flexGrow: 1, + }, + itemContainer: { + flexShrink: 0, + }, + itemContainerCentered: { + flexGrow: 1, + }, + item: { paddingTop: 10, paddingHorizontal: ITEM_PADDING, justifyContent: 'center', }, + itemCentered: { + flexGrow: 1, + }, itemInner: { alignItems: 'center', flexGrow: 1, diff --git a/src/view/com/pager/TabBar.web.tsx b/src/view/com/pager/TabBar.web.tsx index dbe76cb89f..3d3e94429c 100644 --- a/src/view/com/pager/TabBar.web.tsx +++ b/src/view/com/pager/TabBar.web.tsx @@ -11,6 +11,7 @@ export interface TabBarProps { testID?: string selectedPage: number items: string[] + align?: 'center' | 'left' indicatorColor?: string backgroundColor?: string @@ -34,6 +35,7 @@ export function TabBar({ testID, selectedPage, items, + align = 'center', onSelect, onPressSelected, }: TabBarProps) { @@ -87,7 +89,7 @@ export function TabBar({ behavior: 'smooth', }) } - }, [scrollElRef, selectedPage, styles]) + }, [scrollElRef, selectedPage, styles, align]) const onPressItem = useCallback( (index: number) => { @@ -119,7 +121,7 @@ export function TabBar({ ref={node => { itemRefs.current[i] = node as any }} - style={styles.item} + style={[styles.item, align === 'left' && leftAlignedStyles.item]} hoverStyle={t.atoms.bg_contrast_25} onPress={() => onPressItem(i)} accessibilityRole="tab"> @@ -239,3 +241,10 @@ const mobileStyles = StyleSheet.create({ borderBottomWidth: StyleSheet.hairlineWidth, }, }) + +const leftAlignedStyles = StyleSheet.create({ + item: { + flexGrow: 0, + flexShrink: 0, + }, +}) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 85eb1fb627..5806548342 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -1077,15 +1077,22 @@ let PostFeed = ({ onPostSeen(post) - // Only track the root post of each slice (index 0) to avoid double-counting thread items - if (indexInSlice === 0 && !seenPostUrisRef.current.has(post.uri)) { + // Track the post selected by the feed once it is actually visible. + if ( + post.uri === slice.feedPostUri && + !seenPostUrisRef.current.has(post.uri) + ) { seenPostUrisRef.current.add(post.uri) - const position = getPostPosition('sliceItem', item.key) + const position = getPostPosition( + 'sliceItem', + slice.items[0]._reactKey, + ) ax.metric('post:view', { uri: post.uri, authorDid: post.author.did, + isReply: !!postItem.record.reply, logContext: 'FeedItem', feedDescriptor: feedFeedback.feedDescriptor || feed, position, @@ -1121,6 +1128,7 @@ let PostFeed = ({ ax.metric('post:view', { uri: post.uri, authorDid: post.author.did, + isReply: !!postItem.record.reply, logContext: 'FeedItem', feedDescriptor: feedFeedback.feedDescriptor || feed, position, diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 3a66564ed9..a5c16d5a94 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -44,6 +44,7 @@ import * as ReportDialogMetadataContext from '#/components/moderation/ReportDial import {type AppModerationCause} from '#/components/Pills' import {Embed} from '#/components/Post/Embed' import {PostEmbedViewContext} from '#/components/Post/Embed/types' +import {KnownLikers} from '#/components/Post/KnownLikers' import {PostRepliedTo} from '#/components/Post/PostRepliedTo' import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' import {TranslatedPost} from '#/components/Post/Translated' @@ -51,7 +52,7 @@ import {PostControls} from '#/components/PostControls' import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' import {RichText} from '#/components/RichText' import {SubtleHover} from '#/components/SubtleHover' -import {useAnalytics} from '#/analytics' +import {Features, useAnalytics} from '#/analytics' import {useActorStatus} from '#/features/liveNow' import {app} from '#/lexicons' import * as bsky from '#/types/bsky' @@ -456,6 +457,11 @@ let FeedItemInner = ({ onShowLess={onShowLess} viaRepost={viaRepost} /> + diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index 11d69f2277..b51113da4f 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -8,7 +8,6 @@ import {type NavigationProp} from '#/lib/routes/types' import {shareText, shareUrl} from '#/lib/sharing' import {toShareUrl} from '#/lib/strings/url-helpers' import {type Shadow} from '#/state/cache/types' -import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' import { RQKEY as profileQueryKey, useProfileBlockMutationQueue, @@ -18,7 +17,6 @@ import { } from '#/state/queries/profile' import {useSession} from '#/state/session' import {EventStopper} from '#/view/com/util/EventStopper' -import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {UserAddRemoveListsDialog} from '#/components/dialogs/lists/UserAddRemoveListsDialog' @@ -63,8 +61,6 @@ import {useActorStatus, useLiveNowConfig} from '#/features/liveNow' import {EditLiveDialog} from '#/features/liveNow/components/EditLiveDialog' import {GoLiveDialog} from '#/features/liveNow/components/GoLiveDialog' import {GoLiveDisabledDialog} from '#/features/liveNow/components/GoLiveDisabledDialog' -import {Dot} from '#/features/nuxs/components/Dot' -import {Gradient} from '#/features/nuxs/components/Gradient' import {type app} from '#/lexicons' import {useDevMode} from '#/storage/hooks/dev-mode' @@ -73,7 +69,6 @@ let ProfileMenu = ({ }: { profile: Shadow }): React.ReactNode => { - const t = useTheme() const ax = useAnalytics() const {t: l} = useLingui() const {currentAccount, hasSession} = useSession() @@ -89,13 +84,6 @@ let ProfileMenu = ({ const verification = useFullVerificationState({profile}) const {canGoLive} = useLiveNowConfig() const status = useActorStatus(profile) - const statusNudge = useNux(Nux.LiveNowBetaNudge) - const statusNudgeActive = - isSelf && - canGoLive && - statusNudge.status === 'ready' && - !statusNudge.nux?.completed - const {mutate: saveNux} = useSaveNux() const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) const [queueMuteReposts, queueUnmuteReposts] = @@ -292,24 +280,20 @@ let ProfileMenu = ({ {({props}) => { return ( - <> - - {statusNudgeActive && } - + ) }} @@ -381,10 +365,10 @@ let ProfileMenu = ({ )} - Add to starter packs + Add to Starter Packs @@ -413,13 +397,7 @@ let ProfileMenu = ({ } else { goLiveDialogControl.open() } - saveNux({ - id: Nux.LiveNowBetaNudge, - data: undefined, - completed: true, - }) }}> - {statusNudgeActive && } {status.isDisabled ? ( Go live (disabled) @@ -429,26 +407,7 @@ let ProfileMenu = ({ Go live )} - {statusNudgeActive && ( - - New - - )} - t.palette.primary_500 - : undefined - } - /> + )} {verification.viewer.role === 'verifier' && diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index 70b4791230..ea4f7d5f1b 100644 --- a/src/view/com/profile/ProfileSubpageHeader.tsx +++ b/src/view/com/profile/ProfileSubpageHeader.tsx @@ -1,9 +1,7 @@ import {useCallback} from 'react' import {Pressable, View} from 'react-native' import Animated, {useAnimatedRef} from 'react-native-reanimated' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {usePalette} from '#/lib/hooks/usePalette' @@ -47,7 +45,7 @@ export function ProfileSubpageHeader({ avatarType: UserAvatarType | 'starter-pack' }>) { const navigation = useNavigation() - const {_} = useLingui() + const {t: l} = useLingui() const {isMobile} = useWebMediaQueries() const {openLightbox} = useLightboxControls() const pal = usePalette('default') @@ -90,7 +88,6 @@ export function ProfileSubpageHeader({ {children} - {avatarType === 'starter-pack' ? ( @@ -166,10 +163,10 @@ export function ProfileSubpageHeader({ ) ) : purpose === 'app.bsky.graph.defs#referencelist' ? ( isOwner ? ( - Starter pack by you + Starter Pack by you ) : ( - Starter pack by{' '} + Starter Pack by{' '} { for (const entry of list.getEntries()) { - taskCount++ - totalDurationMs += entry.duration - maxDurationMs = Math.max(maxDurationMs, entry.duration) + stats.taskCount++ + stats.totalDurationMs += entry.duration + stats.maxDurationMs = Math.max(stats.maxDurationMs, entry.duration) } }) @@ -560,18 +558,18 @@ function useWebListTelemetry({ } const report = () => { - if (taskCount === 0) return + if (stats.taskCount === 0) return ax.metric('web:list:longTasks', { itemCount: itemCountRef.current, - taskCount, - totalDurationMs: Math.round(totalDurationMs), - maxDurationMs: Math.round(maxDurationMs), + taskCount: stats.taskCount, + totalDurationMs: Math.round(stats.totalDurationMs), + maxDurationMs: Math.round(stats.maxDurationMs), intervalMs: LONG_TASK_REPORT_INTERVAL, ...getWebListDiagnostics(containerRef, rowNodesRef), }) - taskCount = 0 - totalDurationMs = 0 - maxDurationMs = 0 + stats.taskCount = 0 + stats.totalDurationMs = 0 + stats.maxDurationMs = 0 } const interval = setInterval(report, LONG_TASK_REPORT_INTERVAL) return () => { diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 6122757f71..3250ee3782 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -567,11 +567,11 @@ let PreviewableUserAvatar = ({ unstableCacheProfileView(queryClient, profile) }, [profile, queryClient, onBeforePress]) - const onOpenLiveStatus = useCallback(() => { + const onOpenLiveStatus = () => { playHaptic('Light') ax.metric('live:card:open', {subject: profile.did, from: 'post'}) liveControl.open() - }, [liveControl, playHaptic, profile.did]) + } const avatarEl = ( pinnedFeedInfos.map(f => f.feedDescriptor), @@ -125,13 +132,14 @@ function HomeScreenReady({ const maybeFoundIndex = allFeeds.indexOf(maybeRawSelectedFeed) const selectedIndex = Math.max(0, maybeFoundIndex) const maybeSelectedFeed: FeedDescriptor | undefined = allFeeds[selectedIndex] + const selectedFeedInfo = pinnedFeedInfos[selectedIndex] const requestNotificationsPermission = useRequestNotificationsPermission() - useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName) + useSetTitle(selectedFeedInfo && getLocalizedFeedName(selectedFeedInfo, i18n)) useOTAUpdates() useEffect(() => { - requestNotificationsPermission('Home') + void requestNotificationsPermission('Home') }, [requestNotificationsPermission]) const pagerRef = useRef(null) @@ -223,8 +231,13 @@ function HomeScreenReady({ {...props} testID="homeScreenFeedTabs" onPressSelected={onPressSelected} - // @ts-expect-error - feeds={[{displayName: 'Following'}, {displayName: 'Discover'}]} + feeds={[ + { + displayName: 'Following', + uri: TIMELINE_SAVED_FEED.value, + }, + {displayName: 'Discover', uri: DISCOVER_FEED_URI}, + ]} /> ) } diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx index da3463233c..b35ba4d862 100644 --- a/src/view/screens/ModerationBlockedAccounts.tsx +++ b/src/view/screens/ModerationBlockedAccounts.tsx @@ -36,7 +36,7 @@ export function ModerationBlockedAccounts({}: Props) { fetchNextPage, isFetchingNextPage, } = useMyBlockedAccountsQuery() - const isEmpty = !isFetching && !data?.pages[0]?.blocks.length + const isEmpty = !isFetching && !data?.pages.some(page => page.blocks.length) const profiles = useMemo(() => { if (data?.pages) { return data.pages.flatMap(page => page.blocks) diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx index cfdc4a7a80..7184d31b84 100644 --- a/src/view/screens/ModerationMutedAccounts.tsx +++ b/src/view/screens/ModerationMutedAccounts.tsx @@ -36,7 +36,7 @@ export function ModerationMutedAccounts({}: Props) { fetchNextPage, isFetchingNextPage, } = useMyMutedAccountsQuery() - const isEmpty = !isFetching && !data?.pages[0]?.mutes.length + const isEmpty = !isFetching && !data?.pages.some(page => page.mutes.length) const profiles = useMemo(() => { if (data?.pages) { return data.pages.flatMap(page => page.mutes) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 70b223a08e..e2eb20b1b3 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -349,7 +349,7 @@ function ProfileScreenLoaded({ const wrappedNavToWizard = requireEmailVerification(navToWizard, { instructions: [ - Before creating a starter pack, you must first verify your email. + Before creating a Starter Pack, you must first verify your email. , ], }) diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx index 8e4e98addc..2babcfbcb0 100644 --- a/src/view/shell/desktop/Feeds.tsx +++ b/src/view/shell/desktop/Feeds.tsx @@ -1,10 +1,10 @@ import {Pressable, View} from 'react-native' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' +import {useLingui} from '@lingui/react/macro' import {useNavigation, useNavigationState} from '@react-navigation/native' import {getCurrentRoute} from '#/lib/routes/helpers' import {type NavigationProp} from '#/lib/routes/types' +import {getLocalizedFeedName} from '#/lib/strings/feed-names' import {emitSoftReset} from '#/state/events' import { type SavedFeedSourceInfo, @@ -22,7 +22,7 @@ import {useAnalytics} from '#/analytics' export function DesktopFeeds() { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() const ax = useAnalytics() const {data: pinnedFeedInfos, error, isLoading} = usePinnedFeedsInfos() const selectedFeed = useSelectedFeed() @@ -100,10 +100,9 @@ export function DesktopFeeds() { /> ) })} - - {_(msg`More feeds`)} + {l`More feeds`} ) @@ -170,19 +169,20 @@ function FeedItem({ onPress: () => void }) { const t = useTheme() - const {_} = useLingui() + const {t: l, i18n} = useLingui() const { state: hovered, onIn: onHoverIn, onOut: onHoverOut, } = useInteractionState() const isFollowing = feedInfo.feedDescriptor === 'following' + const displayName = getLocalizedFeedName(feedInfo, i18n) return ( - {feedInfo.displayName} + {displayName} ) diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 585a4afff5..7ad99d805f 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -559,9 +559,8 @@ function ComposeBtn({minimal}: {minimal: boolean}) { handle = await fetchHandle(handle) } catch (e) { handle = undefined - } finally { - setIsFetchingHandle(false) } + setIsFetchingHandle(false) } if ( diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 9603236d4d..c74a055499 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -40,6 +40,7 @@ import { } from '#/components/PolicyUpdateOverlay' import {Outlet as PortalOutlet} from '#/components/Portal' import {useAgeAssurance} from '#/ageAssurance' +import {DataUnavailableScreen} from '#/ageAssurance/components/DataUnavailableScreen' import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen' import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay' import {PassiveAnalytics} from '#/analytics/PassiveAnalytics' @@ -245,7 +246,9 @@ export function Shell() { ) : ( <> - {aa.state.access === aa.Access.None ? ( + {aa.state.error === 'account-data' ? ( + + ) : aa.state.access === aa.Access.None ? ( ) : ( diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 6a68530f05..325e665761 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -30,6 +30,7 @@ import { import {Outlet as PortalOutlet} from '#/components/Portal' import {WelcomeModal} from '#/components/WelcomeModal' import {useAgeAssurance} from '#/ageAssurance' +import {DataUnavailableScreen} from '#/ageAssurance/components/DataUnavailableScreen' import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen' import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay' import {PassiveAnalytics} from '#/analytics/PassiveAnalytics' @@ -167,7 +168,9 @@ export function Shell() { ) : ( <> - {aa.state.access === aa.Access.None ? ( + {aa.state.error === 'account-data' ? ( + + ) : aa.state.access === aa.Access.None ? ( ) : (