Merge remote-tracking branch 'origin/main' into app-2662-add-client-events-for-unrenderable-video-playback

# Conflicts:
#	src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
This commit is contained in:
vineyardbovines
2026-07-21 11:24:21 -04:00
142 changed files with 3301 additions and 1138 deletions
+15
View File
@@ -0,0 +1,15 @@
---
name: Compile translations
description: Compile i18n translations and fail on compilation errors.
runs:
using: composite
steps:
- name: 🔤 Compile translations
shell: bash
run: pnpm intl:build 2>&1 | tee i18n.log
- name: Check for i18n compilation errors
shell: bash
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
@@ -0,0 +1,72 @@
---
name: Local EAS Build
description: Build an Expo app locally with a selected EAS profile.
inputs:
platform:
description: EAS platform to build (ios or android)
required: true
profile:
description: EAS build profile
required: true
output:
description: Output path for the local build artifact
required: true
log-path:
description: Optional path to tee build output into
required: false
default: ""
bump-build-number:
description: Run the build through use-build-number-with-bump
required: false
default: "false"
sentry-auth-token:
description: Optional Sentry authentication token
required: false
default: ""
sentry-release:
description: Optional Sentry release
required: false
default: ""
sentry-dist:
description: Optional Sentry distribution
required: false
default: ""
runs:
using: composite
steps:
- name: Build locally with EAS
shell: bash
env:
PLATFORM: ${{ inputs.platform }}
PROFILE: ${{ inputs.profile }}
OUTPUT: ${{ inputs.output }}
LOG_PATH: ${{ inputs.log-path }}
BUMP_BUILD_NUMBER: ${{ inputs.bump-build-number }}
SENTRY_AUTH_TOKEN: ${{ inputs.sentry-auth-token }}
SENTRY_RELEASE: ${{ inputs.sentry-release }}
SENTRY_DIST: ${{ inputs.sentry-dist }}
run: |
set -o pipefail
build_command=(
pnpm eas build
--platform "$PLATFORM"
--profile "$PROFILE"
--local
--output "$OUTPUT"
--non-interactive
)
if [ -n "$LOG_PATH" ]; then
mkdir -p "$(dirname "$LOG_PATH")"
if [ "$BUMP_BUILD_NUMBER" = "true" ]; then
pnpm use-build-number-with-bump "${build_command[@]}" 2>&1 | tee "$LOG_PATH"
else
"${build_command[@]}" 2>&1 | tee "$LOG_PATH"
fi
elif [ "$BUMP_BUILD_NUMBER" = "true" ]; then
pnpm use-build-number-with-bump "${build_command[@]}"
else
"${build_command[@]}"
fi
@@ -0,0 +1,47 @@
---
name: Setup Expo Project
description: Install dependencies and set up the Expo/EAS CLI for a build. Does not check out the repo.
inputs:
expo-token:
description: Expo token (EXPO_TOKEN secret)
required: true
eas-version:
description: EAS CLI version to install
required: false
default: '19.0.5'
runs:
using: composite
steps:
- name: Check for EXPO_TOKEN
shell: bash
env:
EXPO_TOKEN: ${{ inputs.expo-token }}
run: >
if [ -z "$EXPO_TOKEN" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: package.json
cache: pnpm
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1
- name: ⚙️ Install dependencies
shell: bash
run: pnpm install --frozen-lockfile
- name: 🔨 Setup Expo CLI
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
eas-version: ${{ inputs.eas-version }}
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ inputs.expo-token }}
+63
View File
@@ -0,0 +1,63 @@
---
name: Write Environment Variables
description: Write the .env file and google-services.json used by the build.
inputs:
env-token:
description: Base .env contents (ENV_TOKEN secret)
required: true
sentry-dsn:
description: Sentry DSN (SENTRY_DSN secret)
required: true
bitdrift-api-key:
description: Bitdrift API key (BITDRIFT_API_KEY secret)
required: true
gcp-project-id:
description: GCP project ID (EXPO_PUBLIC_GCP_PROJECT_ID secret)
required: true
google-services-token:
description: google-services.json contents (GOOGLE_SERVICES_TOKEN secret)
required: true
expo-public-env:
description: >
EXPO_PUBLIC_ENV value. Only set for OTA deploys where eas.json isn't used;
for regular builds this is normally handled in eas.json.
required: false
default: ''
outputs:
release-version:
description: Version from package.json
value: ${{ steps.env.outputs.release-version }}
bundle-identifier:
description: git SHA of HEAD
value: ${{ steps.env.outputs.bundle-identifier }}
runs:
using: composite
steps:
- name: ✏️ Write environment variables
id: env
shell: bash
env:
ENV_TOKEN: ${{ inputs.env-token }}
SENTRY_DSN: ${{ inputs.sentry-dsn }}
BITDRIFT_API_KEY: ${{ inputs.bitdrift-api-key }}
GCP_PROJECT_ID: ${{ inputs.gcp-project-id }}
GOOGLE_SERVICES_TOKEN: ${{ inputs.google-services-token }}
EXPO_PUBLIC_ENV: ${{ inputs.expo-public-env }}
run: |
echo "$ENV_TOKEN" > .env
# EXPO_PUBLIC_ENV is normally handled in eas.json; only written here for OTA deploys.
if [ -n "$EXPO_PUBLIC_ENV" ]; then
echo "EXPO_PUBLIC_ENV=$EXPO_PUBLIC_ENV" >> .env
fi
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "release-version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "bundle-identifier=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=$SENTRY_DSN" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=$BITDRIFT_API_KEY" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=$GCP_PROJECT_ID" >> .env
echo "$GOOGLE_SERVICES_TOKEN" > google-services.json
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set +e
platform="${1:?usage: cleanup-nightly-e2e.sh <ios|android> <device-id>}"
device_id="${2:-}"
artifact_dir="${GITHUB_WORKSPACE:-$PWD}/artifacts/$platform"
mkdir -p "$artifact_dir"
if [[ -f i18n.log ]]; then
cp i18n.log "$artifact_dir/i18n.log"
fi
stop_process_tree() {
local pid="$1"
local child
while read -r child; do
[[ -n "$child" ]] && stop_process_tree "$child"
done < <(pgrep -P "$pid" 2>/dev/null || true)
kill -TERM "$pid" >/dev/null 2>&1 || true
}
stop_pid_file() {
[[ -f "$1" ]] || return 0
local pid
pid="$(cat "$1")"
stop_process_tree "$pid"
}
stop_pid_file "$artifact_dir/logcat.pid"
stop_pid_file "$artifact_dir/metro.pid"
stop_pid_file "$artifact_dir/mock-server.pid"
stop_pid_file "$artifact_dir/emulator.pid"
if [[ "$platform" == "ios" ]]; then
if [[ -f "$artifact_dir/redis-bin.txt" ]]; then
"$(cat "$artifact_dir/redis-bin.txt")/redis-cli" \
-h 127.0.0.1 -p 6380 shutdown nosave >/dev/null 2>&1 || true
fi
if [[ -f "$artifact_dir/postgres-bin.txt" ]]; then
"$(cat "$artifact_dir/postgres-bin.txt")/pg_ctl" \
-D "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" -m fast stop >/dev/null 2>&1 || true
fi
[[ -n "$device_id" ]] && xcrun simctl shutdown "$device_id" >/dev/null 2>&1 || true
else
docker compose -f dev-env/dev-infra/docker-compose.yaml logs --no-color \
>>"$artifact_dir/docker-services.log" 2>&1 || true
docker compose -f dev-env/dev-infra/docker-compose.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true
[[ -n "$device_id" ]] && adb -s "$device_id" emu kill >/dev/null 2>&1 || true
fi
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env bash
set -Eeuo pipefail
platform="${1:?usage: run-nightly-e2e.sh <ios|android> <device-id>}"
device_id="${2:?usage: run-nightly-e2e.sh <ios|android> <device-id>}"
if [[ "$platform" != "ios" && "$platform" != "android" ]]; then
echo "Unsupported platform: $platform" >&2
exit 2
fi
artifact_dir="${GITHUB_WORKSPACE:-$PWD}/artifacts/$platform"
maestro_dir="$artifact_dir/maestro"
mkdir -p "$maestro_dir"
phase() {
printf '%s\n' "$1" >"$artifact_dir/phase.txt"
}
wait_for_port() {
local port="$1"
local label="$2"
local attempts="${3:-120}"
for ((i = 1; i <= attempts; i++)); do
if nc -z 127.0.0.1 "$port" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
echo "Timed out waiting for $label on port $port" >&2
return 1
}
# shellcheck disable=SC2329 # Invoked through the cleanup trap call chain.
stop_process_tree() {
local pid="$1"
local child
while read -r child; do
[[ -n "$child" ]] && stop_process_tree "$child"
done < <(pgrep -P "$pid" 2>/dev/null || true)
kill -TERM "$pid" >/dev/null 2>&1 || true
}
# shellcheck disable=SC2329 # Invoked by cleanup, which is registered as a trap.
stop_pid_file() {
local pid_file="$1"
[[ -f "$pid_file" ]] || return 0
local pid
pid="$(cat "$pid_file")"
[[ -n "$pid" ]] || return 0
# pnpm and Expo both spawn multiple generations of children.
stop_process_tree "$pid"
}
# shellcheck disable=SC2329 # Invoked by the EXIT/INT/TERM trap below.
cleanup() {
set +e
stop_pid_file "$artifact_dir/logcat.pid"
stop_pid_file "$artifact_dir/metro.pid"
stop_pid_file "$artifact_dir/mock-server.pid"
if [[ "$platform" == "ios" ]]; then
if [[ -f "$artifact_dir/redis.pid" ]]; then
redis_bin="$(cat "$artifact_dir/redis-bin.txt")"
"$redis_bin/redis-cli" -h 127.0.0.1 -p 6380 shutdown nosave >/dev/null 2>&1 || true
fi
if [[ -f "$artifact_dir/postgres-bin.txt" && -d "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" ]]; then
postgres_bin="$(cat "$artifact_dir/postgres-bin.txt")"
"$postgres_bin/pg_ctl" -D "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" -m fast stop >/dev/null 2>&1 || true
fi
else
docker compose -f dev-env/dev-infra/docker-compose.yaml logs --no-color \
>>"$artifact_dir/docker-services.log" 2>&1 || true
docker compose -f dev-env/dev-infra/docker-compose.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT INT TERM
if [[ "$platform" == "android" ]]; then
adb -s "$device_id" logcat -c
adb -s "$device_id" logcat -v threadtime >"$artifact_dir/logcat.log" 2>&1 &
printf '%s\n' "$!" >"$artifact_dir/logcat.pid"
fi
phase "Starting PostgreSQL, Redis, and mock server"
if [[ "$platform" == "ios" ]]; then
brew install postgresql@14 2>&1 | tee "$artifact_dir/native-dependencies.log"
postgres_bin="$(brew --prefix postgresql@14)/bin"
redis_version="7.4.7"
redis_archive="${RUNNER_TEMP:-/tmp}/redis-${redis_version}.tar.gz"
redis_source="${RUNNER_TEMP:-/tmp}/redis-${redis_version}"
curl -fsSL -o "$redis_archive" \
"https://download.redis.io/releases/redis-${redis_version}.tar.gz"
echo "c97e57b0df330a9e091cacff012bebe763c275398cf36ff44cdba876814b595b $redis_archive" \
| shasum -a 256 --check | tee -a "$artifact_dir/native-dependencies.log"
rm -rf "$redis_source"
tar -xzf "$redis_archive" -C "${RUNNER_TEMP:-/tmp}"
make -C "$redis_source" -j "$(sysctl -n hw.ncpu)" \
2>&1 | tee -a "$artifact_dir/native-dependencies.log"
redis_bin="$redis_source/src"
"$redis_bin/redis-server" --version | tee -a "$artifact_dir/native-dependencies.log"
printf '%s\n' "$redis_bin" >"$artifact_dir/redis-bin.txt"
postgres_data="${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres"
rm -rf "$postgres_data"
"$postgres_bin/initdb" -D "$postgres_data" --auth=trust --username=pg --no-locale \
>"$artifact_dir/postgres-init.log" 2>&1
"$postgres_bin/pg_ctl" -D "$postgres_data" \
-o "-p 5433 -h 127.0.0.1" -l "$artifact_dir/postgres.log" start
printf '%s\n' "$postgres_bin" >"$artifact_dir/postgres-bin.txt"
"$redis_bin/redis-server" \
--bind 127.0.0.1 \
--port 6380 \
--save "" \
--appendonly no \
--daemonize yes \
--pidfile "$artifact_dir/redis.pid" \
--logfile "$artifact_dir/redis.log"
wait_for_port 5433 "PostgreSQL"
wait_for_port 6380 "Redis"
pnpm --dir dev-env start:external >"$artifact_dir/mock-server.log" 2>&1 &
else
pnpm --dir dev-env start >"$artifact_dir/mock-server.log" 2>&1 &
fi
printf '%s\n' "$!" >"$artifact_dir/mock-server.pid"
wait_for_port 1986 "the E2E mock-server manager"
phase "Starting Metro"
EXPO_PUBLIC_ENV=e2e \
NODE_ENV=test \
RN_SRC_EXT=e2e.ts,e2e.tsx \
pnpm exec expo start --dev-client --clear --port 8081 \
>"$artifact_dir/metro.log" 2>&1 &
printf '%s\n' "$!" >"$artifact_dir/metro.pid"
wait_for_port 8081 "Metro"
# Pre-warm Metro bundle so the first Maestro flow doesn't hit a cold-start delay
phase "Pre-warming Metro bundle"
bundle_platform="$platform"
curl -s -o /dev/null "http://localhost:8081/index.bundle?platform=${bundle_platform}&dev=true&minify=false"
echo "Metro bundle pre-warmed for $bundle_platform"
if [[ "$platform" == "android" ]]; then
phase "Configuring Android localhost routing"
adb -s "$device_id" reverse tcp:3000 tcp:3000
adb -s "$device_id" reverse tcp:8081 tcp:8081
fi
phase "Running Maestro flows"
set +e
maestro test \
--udid "$device_id" \
--format JUNIT \
--output "$artifact_dir/report.xml" \
--config __e2e__/config.yml \
--debug-output "$maestro_dir" \
--test-output-dir "$maestro_dir" \
--flatten-debug-output \
__e2e__ \
2>&1 | tee "$artifact_dir/maestro-cli.log"
maestro_status=${PIPESTATUS[0]}
set -e
if [[ "$maestro_status" -eq 0 ]]; then
phase "Completed"
else
phase "Maestro flow failure"
fi
exit "$maestro_status"
+356
View File
@@ -0,0 +1,356 @@
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
const ENTITY_REPLACEMENTS = {
'&amp;': '&',
'&apos;': "'",
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
}
function decodeXml(value = '') {
return value
.replace(/&(amp|apos|gt|lt|quot);/g, entity => ENTITY_REPLACEMENTS[entity])
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
.replace(/&#x([\da-f]+);/gi, (_, code) =>
String.fromCodePoint(Number.parseInt(code, 16)),
)
}
function attributes(source = '') {
const result = {}
for (const match of source.matchAll(/([\w:.-]+)\s*=\s*(["'])(.*?)\2/gs)) {
result[match[1]] = decodeXml(match[3])
}
return result
}
function concise(value, limit = 300) {
const normalized = decodeXml(value)
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim()
return normalized.length > limit
? `${normalized.slice(0, limit - 1)}…`
: normalized
}
export function parseJUnit(xml) {
const failures = []
const testcasePattern = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/gi
for (const match of xml.matchAll(testcasePattern)) {
const testcase = attributes(match[1])
const body = match[2] || ''
const failure = body.match(/<(failure|error)\b([^>]*)>([\s\S]*?)<\/\1>/i)
const selfClosingFailure = body.match(/<(failure|error)\b([^>]*)\/>/i)
const failureMatch = failure || selfClosingFailure
if (!failureMatch) continue
const failureAttributes = attributes(failureMatch[2])
const name = testcase.name || testcase.classname || 'Unnamed Maestro flow'
const message = concise(
failureAttributes.message || (failure ? failureMatch[3] : '') || 'Failed',
)
failures.push({name, message})
}
if (failures.length === 0) {
const suite = xml.match(/<testsuite\b([^>]*)>/i)
const suiteAttributes = attributes(suite?.[1])
if (
Number(suiteAttributes.failures || 0) +
Number(suiteAttributes.errors || 0) >
0
) {
failures.push({
name: suiteAttributes.name || 'Maestro test suite',
message: 'JUnit reported a failure without testcase details',
})
}
}
return failures
}
export function parseMaestroCli(log) {
const failures = []
const failurePattern = /^\[Failed\]\s+(.+?)\s+\([^)]*\)\s+\((.+)\)\s*$/gm
for (const match of log.matchAll(failurePattern)) {
failures.push({
name: concise(match[1], 120),
message: concise(match[2]),
})
}
return failures
}
function walk(root) {
if (!root || !fs.existsSync(root)) return []
const entries = fs.readdirSync(root, {withFileTypes: true})
return entries.flatMap(entry => {
const candidate = path.join(root, entry.name)
return entry.isDirectory() ? walk(candidate) : [candidate]
})
}
function readPhase(root) {
const phaseFile = walk(root).find(file => path.basename(file) === 'phase.txt')
return phaseFile ? fs.readFileSync(phaseFile, 'utf8').trim() : ''
}
function platformResult({name, status, root, artifactUrl}) {
const files = walk(root)
const reports = files.filter(file => /(?:report|junit).*\.xml$/i.test(file))
const junitFailures = reports.flatMap(report =>
parseJUnit(fs.readFileSync(report, 'utf8')),
)
const maestroLogs = files.filter(
file => path.basename(file) === 'maestro-cli.log',
)
const cliFailures = maestroLogs.flatMap(log =>
parseMaestroCli(fs.readFileSync(log, 'utf8')),
)
// 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
// A skipped platform (e.g. iOS while temporarily disabled) is not a failure
// as long as it produced no flow failures.
const failed =
(status !== 'success' && status !== 'skipped') || failures.length > 0
return {
name,
status,
failed,
failures,
phase: readPhase(root),
hasJUnit: reports.length > 0,
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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
}
function platformBlock(platform) {
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'}`,
'',
`- Commit: [\`${shortSha}\`](${commitUrl})`,
`- Workflow run: [open run](${runUrl})`,
'',
]
for (const platform of platforms) {
const headerEmoji =
platform.status === 'success'
? '✅'
: platform.status === 'skipped'
? '⏭️'
: '❌'
lines.push(
`## ${headerEmoji} ${platform.name}`,
'',
`Job status: \`${platform.status}\``,
'',
)
if (platform.failures.length > 0) {
for (const failure of platform.failures.slice(0, 10)) {
lines.push(`- **${failure.name}:** ${failure.message}`)
}
if (platform.failures.length > 10) {
lines.push(`- …and ${platform.failures.length - 10} more failed flows`)
}
lines.push('')
} 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.name} logs and artifacts](${platform.artifactUrl})`,
'',
)
}
}
return lines.join('\n').trim()
}
export function buildSummary({
iosStatus,
androidStatus,
iosRoot,
androidRoot,
artifactUrls = {},
sha,
runUrl,
commitUrl,
}) {
const platforms = [
platformResult({
name: 'iOS',
status: iosStatus,
root: iosRoot,
artifactUrl: artifactUrls.ios,
}),
platformResult({
name: 'Android',
status: androidStatus,
root: androidRoot,
artifactUrl: artifactUrls.android,
}),
]
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'}] : []),
]),
]
return {
notify,
platforms,
githubSummary: githubSummary({
notify,
platforms,
shortSha,
runUrl,
commitUrl,
}),
payload: {text, blocks},
}
}
function parseArgs(argv) {
const result = {}
for (let i = 0; i < argv.length; i += 2) {
const key = argv[i]
if (!key?.startsWith('--') || argv[i + 1] === undefined) {
throw new Error(`Invalid argument: ${key || '<missing>'}`)
}
result[key.slice(2)] = argv[i + 1]
}
return result
}
if (
process.argv[1] &&
path.resolve(process.argv[1]) === path.resolve(import.meta.filename)
) {
const args = parseArgs(process.argv.slice(2))
const artifactUrls = args['artifact-urls']
? JSON.parse(fs.readFileSync(args['artifact-urls'], 'utf8'))
: {}
const summary = buildSummary({
iosStatus: args['ios-status'],
androidStatus: args['android-status'],
iosRoot: args['ios-root'],
androidRoot: args['android-root'],
artifactUrls,
sha: args.sha,
runUrl: args['run-url'],
commitUrl: args['commit-url'],
})
process.stdout.write(`${JSON.stringify(summary)}\n`)
}
+124 -84
View File
@@ -10,12 +10,25 @@ on:
options:
- testflight-android
- production
submit:
type: boolean
description: Submit the build to Google Play (disable to only produce the APK artifact)
default: true
workflow_call:
inputs:
profile:
type: string
description: Build profile to use
required: true
submit:
type: boolean
description: Submit the build to Google Play (disable to only produce the APK artifact)
default: true
runner:
type: string
description: Runner for the build job (defaults to Linux-x64-32core)
required: false
default: ''
outputs:
package-version:
description: Version from package.json
@@ -56,93 +69,92 @@ permissions:
jobs:
build:
if: github.repository == 'bluesky-social/social-app'
name: Build and Submit Android
runs-on: Linux-x64-32core
name: Build Android
runs-on: ${{ inputs.runner || 'Linux-x64-32core' }}
concurrency:
group: android-build
cancel-in-progress: false
outputs:
package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
version-code: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
apk-artifact-name: build-${{ steps.timestamp.outputs.time }}.apk
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 5
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: 🔧 Setup Expo project
uses: ./.github/actions/setup-expo-project
with:
node-version-file: package.json
cache: pnpm
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1
- name: ⚙️ Install dependencies
run: pnpm install --frozen-lockfile
- name: 🔨 Setup Expo CLI
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: "temurin"
java-version: "17"
- name: 🔤 Compile translations
run: pnpm intl:build 2>&1 | tee i18n.log
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
uses: ./.github/actions/compile-i18n
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
- name: ✏️ Write environment variables
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
uses: ./.github/actions/write-env
with:
env-token: ${{ secrets.ENV_TOKEN }}
sentry-dsn: ${{ secrets.SENTRY_DSN }}
bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }}
gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
- name: 🏗️ EAS Build
env:
PROFILE: ${{ inputs.profile || 'testflight-android' }}
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
pnpm use-build-number-with-bump
pnpm eas build -p android
--profile $PROFILE
--local --output build.aab --non-interactive
uses: ./.github/actions/eas-local-build
with:
platform: android
profile: ${{ inputs.profile || 'testflight-android' }}
output: build.aab
bump-build-number: "true"
sentry-auth-token: ${{ secrets.SENTRY_AUTH_TOKEN }}
sentry-release: ${{ steps.env.outputs.release-version }}
sentry-dist: ${{ steps.env.outputs.bundle-identifier }}
- name: 📚 Get version from package.json
id: get-build-info
run: bash scripts/setGitHubOutput.sh
# Hands the built bundle off to the submit / universalApk jobs. Retention is
# deliberately short (1 day) since it's only an intra-run handoff artifact.
- name: 🚀 Upload AAB artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: android-aab-${{ github.run_id }}
retention-days: 1
if-no-files-found: error
path: build.aab
submit:
name: Submit to Google Play
runs-on: ubuntu-latest
needs: [build]
# Submit unless explicitly disabled; on events where inputs is empty this still submits.
if: ${{ inputs.submit != false }}
steps:
# eas submit reads app config from the repo, so we need a checkout.
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 5
- name: 🔧 Setup Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: ⬇️ Download AAB artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: android-aab-${{ github.run_id }}
- name: 🚀 Submit to Google Play
env:
PROFILE: ${{ inputs.profile || 'testflight-android' }}
@@ -150,13 +162,49 @@ jobs:
- name: 🔔 Notify Slack of Play Store Submission
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{"text": "Android ${{ inputs.profile || 'testflight-android' }} build submitted to Google Play!\n```Version Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
{"text": "Android ${{ inputs.profile || 'testflight-android' }} build submitted to Google Play!\n```Version Number: ${{ needs.build.outputs.package-version }}\nBuild Number: ${{ needs.build.outputs.version-code }}```"}
# Record the commit only after a successful submit, so a failed submit doesn't
# advance the "most recent testflight" marker.
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ inputs.profile == 'testflight-android' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.profile == 'testflight-android' }}
env:
GITHUB_SHA: ${{ github.sha }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
# Runs in parallel with submit: the QA APK shouldn't be blocked by a Play submission failure.
universalApk:
name: Build universal APK
runs-on: ubuntu-latest
needs: [build]
outputs:
apk-artifact-name: build-${{ steps.timestamp.outputs.time }}.apk
steps:
- name: ⬇️ Download AAB artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: android-aab-${{ github.run_id }}
# 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@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: "temurin"
java-version: "17"
- name: 🔧 Setup bundletool
uses: amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1
@@ -164,19 +212,24 @@ jobs:
version: "1.18.3"
- name: 🔑 Decode keystore
run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode >
keystore.jks
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > keystore.jks
- name: 📦 Build signed universal APK
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
bundletool build-apks \
--bundle=build.aab \
--output=universal.apks \
--mode=universal \
--ks=keystore.jks \
--ks-pass=pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
--ks-key-alias=${{ secrets.ANDROID_KEY_ALIAS }} \
--key-pass=pass:${{ secrets.ANDROID_KEY_PASSWORD }}
--ks-pass=pass:"$ANDROID_KEYSTORE_PASSWORD" \
--ks-key-alias="$ANDROID_KEY_ALIAS" \
--key-pass=pass:"$ANDROID_KEY_PASSWORD"
- name: 📋 Rename to .zip for extraction
run: mv universal.apks universal.zip
@@ -198,27 +251,13 @@ jobs:
path: build.apk
- name: 🔔 Notify Slack of APK Artifact
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{"text": "Android ${{ inputs.profile || 'testflight-android' }} APK is ready for testing!\n```Artifact: ${{ steps.upload-artifact.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ inputs.profile == 'testflight-android' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.profile == 'testflight-android' }}
env:
GITHUB_SHA: ${{ github.sha }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
{"text": "Android ${{ inputs.profile || 'testflight-android' }} APK is ready for testing!\n```Artifact: ${{ steps.upload-artifact.outputs.artifact-url }}\nVersion Number: ${{ needs.build.outputs.package-version }}\nBuild Number: ${{ needs.build.outputs.version-code }}```"}
# Releases are cut from tags named after the version (e.g. "1.124.0"), so when a production
# build is dispatched against such a tag we attach the APK to the matching release. This runs
@@ -226,7 +265,7 @@ jobs:
attachToRelease:
name: Attach APK to GitHub Release
runs-on: ubuntu-latest
needs: [build]
needs: [build, universalApk]
if: ${{ inputs.profile == 'production' && github.ref_type == 'tag' && github.repository == 'bluesky-social/social-app' }}
permissions:
contents: write
@@ -254,7 +293,7 @@ jobs:
if: ${{ steps.release-check.outputs.exists == 'true' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ needs.build.outputs.apk-artifact-name }}
name: ${{ needs.universalApk.outputs.apk-artifact-name }}
- name: 🏷️ Rename APK for release
if: ${{ steps.release-check.outputs.exists == 'true' }}
@@ -265,6 +304,7 @@ jobs:
if: ${{ steps.release-check.outputs.exists == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
APK: Bluesky-${{ needs.build.outputs.package-version }}.apk
run: |
@@ -274,7 +314,7 @@ jobs:
- name: 🔔 Notify Slack of Release Attachment
if: ${{ steps.release-check.outputs.exists == 'true' }}
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
+112 -92
View File
@@ -28,6 +28,11 @@ on:
type: string
description: TestFlight group to assign the build to after submitting ("none" to skip)
default: none
runner:
type: string
description: Runner for the build job (defaults to macos-26-xlarge)
required: false
default: ''
outputs:
package-version:
description: Version from package.json
@@ -66,8 +71,8 @@ permissions:
jobs:
build:
if: github.repository == 'bluesky-social/social-app'
name: Build and Submit iOS
runs-on: macos-26-xlarge
name: Build iOS
runs-on: ${{ inputs.runner || 'macos-26-xlarge' }}
concurrency:
group: ios-build
cancel-in-progress: false
@@ -75,38 +80,15 @@ jobs:
package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
build-number: ${{ steps.ipa-build-number.outputs.build-number }}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 5
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: 🔧 Setup Expo project
uses: ./.github/actions/setup-expo-project
with:
node-version-file: package.json
cache: pnpm
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1
- name: ⚙️ Install dependencies
run: pnpm install --frozen-lockfile
- name: 🔨 Setup Expo CLI
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
expo-token: ${{ secrets.EXPO_TOKEN }}
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
@@ -114,7 +96,7 @@ jobs:
- name: ☕️ Assert Cocoapods version
run: |
EXPECTED=1.16.2
EXPECTED=1.17.0
ACTUAL=$(pod --version)
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "Expected Cocoapods $EXPECTED but runner has $ACTUAL."
@@ -133,38 +115,29 @@ jobs:
key: ${{ runner.os }}-pods-${{ hashFiles('pnpm-lock.yaml') }}
- name: 🔤 Compile translations
run: pnpm intl:build 2>&1 | tee i18n.log
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
uses: ./.github/actions/compile-i18n
# EXPO_PUBLIC_ENV is handled in eas.json
- name: ✏️ Write environment variables
id: env
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
uses: ./.github/actions/write-env
with:
env-token: ${{ secrets.ENV_TOKEN }}
sentry-dsn: ${{ secrets.SENTRY_DSN }}
bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }}
gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
- name: 🏗️ EAS Build
env:
PROFILE: ${{ inputs.profile || 'testflight' }}
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
pnpm use-build-number-with-bump
pnpm eas build -p ios
--profile $PROFILE
--local --output build.tar.gz --non-interactive
uses: ./.github/actions/eas-local-build
with:
platform: ios
profile: ${{ inputs.profile || 'testflight' }}
output: build.tar.gz
bump-build-number: "true"
sentry-auth-token: ${{ secrets.SENTRY_AUTH_TOKEN }}
sentry-release: ${{ steps.env.outputs.release-version }}
sentry-dist: ${{ steps.env.outputs.bundle-identifier }}
- name: 📂 Extract build artifact
run: |
@@ -201,16 +174,6 @@ jobs:
exit 1
fi
- name: 🚀 Deploy
run: pnpm eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa"
- name: 🪲 Upload dSYM to Sentry
run: >
SENTRY_ORG=blueskyweb
SENTRY_PROJECT=app
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
pnpm sentry-cli debug-files upload "$BUILD_DIR/Bluesky.app.dSYM.zip" --include-sources
- name: 📚 Get version from package.json
id: get-build-info
run: bash scripts/setGitHubOutput.sh
@@ -220,6 +183,7 @@ jobs:
# number that actually lands in App Store Connect. `eas build:version:get` reads the
# remote counter, which a --local build does not advance, so it can be off by one —
# using it here would make distribute_only poll for a nonexistent build.
# PlistBuddy is macOS-only, which is why this stays in the build job.
- name: 🔢 Read build number from IPA
id: ipa-build-number
run: |
@@ -235,18 +199,98 @@ jobs:
echo "IPA build number: $build_number"
echo "build-number=$build_number" >> "$GITHUB_OUTPUT"
# Hand the IPA and dSYM off to the submit job. Retention is deliberately short since
# this artifact only exists to bridge the two jobs within a single run.
- name: 🚀 Upload build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ios-build-${{ github.run_id }}
retention-days: 1
if-no-files-found: error
path: |
${{ env.BUILD_DIR }}/Bluesky.ipa
${{ env.BUILD_DIR }}/Bluesky.app.dSYM.zip
submit:
name: Submit iOS
# Submission and dSYM upload are I/O bound and don't need the xlarge builder.
runs-on: macos-26
needs: [build]
steps:
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# eas submit reads the app config from the repo
fetch-depth: 5
- name: 🔧 Setup Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: ⬇️ Download build artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ios-build-${{ github.run_id }}
path: ios-build
- name: 🚀 Deploy
run: pnpm eas submit -p ios --non-interactive --path ios-build/Bluesky.ipa
- name: 🪲 Upload dSYM to Sentry
env:
SENTRY_ORG: blueskyweb
SENTRY_PROJECT: app
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: pnpm sentry-cli debug-files upload ios-build/Bluesky.app.dSYM.zip --include-sources
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ needs.build.outputs.package-version }}\nBuild Number: ${{ needs.build.outputs.build-number }}```"}
# Record the commit only after a successful submit, so a failed submit doesn't advance
# the baseline used for the next testflight build's changelog.
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ inputs.profile == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
env:
GITHUB_SHA: ${{ github.sha }}
if: ${{ inputs.profile == 'testflight' }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
distribute:
name: Assign build to TestFlight group
# fastlane and jq ship preinstalled on the macOS runner image, and this step mostly idles
# polling Apple processing, so it runs on a normal-size runner.
runs-on: macos-26
needs: [build, submit]
# testFlightGroup defaults to 'none' on both workflow_call and dispatch; guard against the
# empty string too, since `!= 'none'` alone would be true for ''.
if: ${{ inputs.testFlightGroup && inputs.testFlightGroup != 'none' }}
steps:
# eas submit only uploads to App Store Connect; it can't assign a build to a
# TestFlight group. fastlane's distribute_only mode skips the upload and assigns the
# already-submitted build to the group, polling until Apple finishes processing it.
- name: 🧪 Assign build to TestFlight group
if: ${{ inputs.testFlightGroup != 'none' }}
env:
TESTFLIGHT_GROUP: ${{ inputs.testFlightGroup }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }}
APP_VERSION: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
BUILD_NUMBER: ${{ steps.ipa-build-number.outputs.build-number }}
APP_VERSION: ${{ needs.build.outputs.package-version }}
BUILD_NUMBER: ${{ needs.build.outputs.build-number }}
run: |
# Ensure the API key material is removed even if fastlane exits non-zero
# (the step runs under `bash -e`, which would otherwise abort before cleanup).
@@ -271,27 +315,3 @@ jobs:
build_number:"$BUILD_NUMBER" \
groups:"$TESTFLIGHT_GROUP" \
notify_external_testers:true
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.ipa-build-number.outputs.build-number }}```"}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ inputs.profile == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
env:
GITHUB_SHA: ${{ github.sha }}
if: ${{ inputs.profile == 'testflight' }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
+60 -314
View File
@@ -86,7 +86,7 @@ jobs:
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
@@ -99,11 +99,7 @@ jobs:
previous-commit-tag: ${{ inputs.runtimeVersion }}
- name: 🔤 Compile translations
run: pnpm intl:build 2>&1 | tee i18n.log
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
uses: ./.github/actions/compile-i18n
- name: Lint check
run: pnpm lint
@@ -128,35 +124,26 @@ jobs:
!steps.version.outputs.version-changed }}
uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1
# eas.json not used here, set EXPO_PUBLIC_ENV
- name: Env
env:
CHANNEL: ${{ inputs.channel || 'testflight' }}
GITHUB_SHA: ${{ github.sha }}
# eas.json not used here, so EXPO_PUBLIC_ENV must be written explicitly
- name: ✏️ Write environment variables
id: env
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_ENV=$CHANNEL" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }}
uses: ./.github/actions/write-env
with:
env-token: ${{ secrets.ENV_TOKEN }}
sentry-dsn: ${{ secrets.SENTRY_DSN }}
bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }}
gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
expo-public-env: ${{ inputs.channel || 'testflight' }}
- name: 🏗️ Create Bundle
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
SENTRY_RELEASE=${{ steps.env.outputs.release-version }}
SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }}
pnpm export
- name: 📦 Package Bundle and 🚀 Deploy
@@ -182,305 +169,64 @@ jobs:
!steps.version.outputs.version-changed }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
name: Build and Submit iOS
runs-on: macos-26
concurrency:
group: ios-build
cancel-in-progress: false
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' &&
needs.bundleDeploy.outputs.changes-detected && github.repository ==
'bluesky-social/social-app' }}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 5
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: package.json
cache: pnpm
- name: 🔨 Setup EAS
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⚙️ Install dependencies
run: pnpm install --frozen-lockfile
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: "26.4"
- name: ☕️ Assert Cocoapods version
run: |
EXPECTED=1.16.2
ACTUAL=$(pod --version)
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "Expected Cocoapods $EXPECTED but runner has $ACTUAL."
echo "The version ships preinstalled with the macOS runner image: https://github.com/actions/runner-images/blob/main/images/macos/macos-26-Readme.md"
echo "If the runner image changed, update EXPECTED here or reinstall the pinned version."
exit 1
fi
- name: 💾 Cache Pods
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: pods-cache
with:
path: ./ios/Pods
# We'll use the pnpm-lock.yaml for our hash since we don't yet have a Podfile.lock. Pod versions will not
# change unless the pnpm version changes as well.
key: ${{ runner.os }}-pods-${{ hashFiles('pnpm-lock.yaml') }}
- name: 🔤 Compile translations
run: pnpm intl:build
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
pnpm use-build-number-with-bump
pnpm eas build -p ios
--profile testflight
--local --output build.tar.gz --non-interactive
- name: 📂 Extract build artifact
run: |
if [ -f "build.tar.gz" ]; then
echo "Extracting build.tar.gz..."
rm -rf ios-build
mkdir -p ios-build
tar -xzf build.tar.gz -C ios-build
echo "Extraction completed successfully"
echo ""
echo "Top-level extracted files:"
find ios-build -maxdepth 3 -print
echo ""
echo "Searching for IPA..."
IPA_PATH="$(find ios-build -type f -name '*.ipa' -print -quit)"
if [ -z "$IPA_PATH" ]; then
echo "ERROR: No .ipa found anywhere under ios-build."
echo "Archive contents:"
tar -tzf build.tar.gz | sed -n '1,200p'
exit 1
fi
BUILD_DIR="$(dirname "$IPA_PATH")"
echo "Found IPA at: $IPA_PATH"
echo "Build dir: $BUILD_DIR"
echo ""
echo "Build dir contents:"
ls -la "$BUILD_DIR"
echo "BUILD_DIR=$BUILD_DIR" >> $GITHUB_ENV
else
echo "Archive file not found!"
exit 1
fi
- name: 🚀 Deploy
run: pnpm eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa"
- name: 🪲 Upload dSYM to Sentry
run: >
SENTRY_ORG=blueskyweb
SENTRY_PROJECT=app
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
pnpm sentry-cli debug-files upload "$BUILD_DIR/Bluesky.app.dSYM.zip" --include-sources
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ inputs.channel == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.channel == 'testflight' }}
env:
GITHUB_SHA: ${{ github.sha }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
uses: ./.github/workflows/build-submit-ios.yml
with:
profile: testflight
testFlightGroup: none
# OTA rebuilds don't need the xlarge builder used for releases
runner: macos-26
# Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`,
# so this workflow never hands the reusable workflow the entire repo secret store.
secrets:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
ENV_TOKEN: ${{ secrets.ENV_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }}
EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }}
SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
buildIfNecessaryAndroid:
name: Build and Submit Android
runs-on: ubuntu-latest
concurrency:
group: android-build
cancel-in-progress: false
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' &&
needs.bundleDeploy.outputs.changes-detected && github.repository ==
'bluesky-social/social-app'}}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 5
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: package.json
cache: pnpm
- name: 🔨 Setup EAS
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
with:
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
with:
distribution: "temurin"
java-version: "17"
- name: ⚙️ Install dependencies
run: pnpm install --frozen-lockfile
- name: 🔤 Compile translations
run: pnpm intl:build
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
echo "$json" > google-services.json
- name: 🏗️ EAS Build
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
pnpm use-build-number-with-bump
pnpm eas build -p android
--profile testflight-android
--local --output build.aab --non-interactive
- name: 📚 Get version from package.json
id: get-build-info
run: bash scripts/setGitHubOutput.sh
- name: 🚀 Submit to Google Play
run: pnpm eas submit -p android --profile testflight-android --non-interactive --path
build.aab
- name: 🔧 Setup bundletool
uses: amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1
with:
version: "1.18.3"
- name: 🔑 Decode keystore
run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode >
keystore.jks
- name: 📦 Build signed universal APK
run: |
bundletool build-apks \
--bundle=build.aab \
--output=universal.apks \
--mode=universal \
--ks=keystore.jks \
--ks-pass=pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
--ks-key-alias=${{ secrets.ANDROID_KEY_ALIAS }} \
--key-pass=pass:${{ secrets.ANDROID_KEY_PASSWORD }}
- name: 📋 Rename to .zip for extraction
run: mv universal.apks universal.zip
- name: 📦 Extract universal APK
run: unzip -p universal.zip universal.apk > build.apk
- name: ⏰ Get a timestamp
id: timestamp
run: echo "time=$(date -u +'%m-%d-%H-%M-%S')" >> "$GITHUB_OUTPUT"
- name: 🚀 Upload Artifact
id: upload-artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
retention-days: 30
compression-level: 0
name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk
- name: 🔔 Notify Slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
env:
GITHUB_SHA: ${{ github.sha }}
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
'bluesky-social/social-app' }}
# build-submit-android.yml contains an attachToRelease job that requests contents: write.
# That job is skipped here (it needs a production tag build), but GitHub statically
# validates the reusable-workflow permission ceiling, so the caller must grant it.
permissions:
contents: write
uses: ./.github/workflows/build-submit-android.yml
with:
profile: testflight-android
runner: ubuntu-latest
# Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`,
# so this workflow never hands the reusable workflow the entire repo secret store.
secrets:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
ENV_TOKEN: ${{ secrets.ENV_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }}
EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}
GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
+2 -2
View File
@@ -59,13 +59,13 @@ jobs:
fetch-depth: 1
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with:
role-to-assume: ${{ secrets.AWS_BEDROCK_REVIEW_ROLE_ARN }}
aws-region: us-east-2
- name: Claude
uses: anthropics/claude-code-action@f87768c6d25f92ae6efa7175e223ef77d4cbf97f # v1.0.166
uses: anthropics/claude-code-action@e90deca47693f9457b72f2b53c17d7c445a87342 # v1.0.171
with:
use_bedrock: 'true'
additional_permissions: |
+2 -2
View File
@@ -45,13 +45,13 @@ jobs:
fetch-depth: 1
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with:
role-to-assume: ${{ secrets.AWS_BEDROCK_REVIEW_ROLE_ARN }}
aws-region: us-east-2
- name: Claude review
uses: anthropics/claude-code-action@f87768c6d25f92ae6efa7175e223ef77d4cbf97f # v1.0.166
uses: anthropics/claude-code-action@e90deca47693f9457b72f2b53c17d7c445a87342 # v1.0.171
with:
use_bedrock: 'true'
additional_permissions: |
+4 -3
View File
@@ -21,7 +21,8 @@ jobs:
strategy:
fail-fast: false
matrix:
job: [lint, prettier, typecheck]
job:
[lint, prettier, 'typecheck:ios', 'typecheck:android', 'typecheck:web']
steps:
- name: Check out Git repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -52,7 +53,7 @@ jobs:
exit $rc
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
@@ -90,7 +91,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
+2 -2
View File
@@ -144,7 +144,7 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: 🔔 Notify Slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5
with:
webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }}
webhook-type: incoming-webhook
@@ -174,7 +174,7 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: 🔔 Notify Slack
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5
with:
webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }}
webhook-type: incoming-webhook
+424
View File
@@ -0,0 +1,424 @@
---
name: Nightly Maestro E2E
on:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: nightly-maestro-e2e-${{ github.ref }}
cancel-in-progress: false
env:
CI: "1"
MAESTRO_VERSION: "2.6.1"
MAESTRO_DRIVER_STARTUP_TIMEOUT: "180000"
MAESTRO_CLI_NO_ANALYTICS: "1"
MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED: "true"
MAESTRO_DISABLE_UPDATE_CHECK: "1"
jobs:
ios:
name: iOS Maestro E2E
if: github.repository == 'bluesky-social/social-app'
runs-on: macos-26-xlarge
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Select Xcode 26.4
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: "26.4"
- name: Prepare E2E configuration
run: |
mkdir -p artifacts/ios
echo "Installing dependencies" > artifacts/ios/phase.txt
cp .env.example .env.test
cp google-services.json.example google-services.json
- name: Set up Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: Set up Java 17
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: temurin
java-version: "17"
- name: Install dev-env dependencies
run: pnpm --dir dev-env install --frozen-lockfile 2>&1 | tee artifacts/ios/dependencies.log
- name: Compile translations
uses: ./.github/actions/compile-i18n
- name: Install Maestro 2.6.1
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" \
| shasum -a 256 --check
unzip -q "$RUNNER_TEMP/maestro.zip" -d "$RUNNER_TEMP"
echo "$RUNNER_TEMP/maestro/bin" >> "$GITHUB_PATH"
"$RUNNER_TEMP/maestro/bin/maestro" --version | tee artifacts/ios/maestro-version.log
test "$("$RUNNER_TEMP/maestro/bin/maestro" --version)" = "$MAESTRO_VERSION"
- name: Boot one iOS simulator
run: |
echo "Booting iOS simulator" > artifacts/ios/phase.txt
device_name="iPhone 17"
runtime_name="iOS 26.5"
runtime_id=$(xcrun simctl list runtimes available --json | jq -r \
--arg name "$runtime_name" \
'[.runtimes[] | select(.name == $name and .isAvailable != false)] | first | .identifier // empty')
if [ -z "$runtime_id" ]; then
echo "The $runtime_name simulator runtime is not installed. Available iOS runtimes:" >&2
xcrun simctl list runtimes available --json | jq -r \
'.runtimes[] | select(.name | startswith("iOS")) | "- \(.name)"' >&2
exit 1
fi
device_type_id=$(xcrun simctl list devicetypes --json | jq -r \
--arg name "$device_name" \
'[.devicetypes[] | select(.name == $name)] | first | .identifier // empty')
if [ -z "$device_type_id" ]; then
echo "The $device_name simulator device type is not installed" >&2
exit 1
fi
udid=$(xcrun simctl list devices available --json | jq -r \
--arg runtime "$runtime_id" \
--arg name "$device_name" \
'[.devices[$runtime][]? | select(.name == $name)] | first | .udid // empty')
if [ -z "$udid" ]; then
udid=$(xcrun simctl create "$device_name" "$device_type_id" "$runtime_id")
fi
echo "IOS_UDID=$udid" >> "$GITHUB_ENV"
xcrun simctl shutdown all || true
xcrun simctl boot "$udid"
xcrun simctl bootstatus "$udid" -b
echo "Using $device_name on $runtime_name ($udid)"
- name: Mark iOS development client build phase
run: echo "Building the iOS development client" > artifacts/ios/phase.txt
- name: Build iOS development client
uses: ./.github/actions/eas-local-build
with:
platform: ios
profile: e2e
output: ${{ runner.temp }}/nightly-e2e-ios.tar.gz
log-path: artifacts/ios/build.log
- name: Install iOS development client
run: |
build_contents="$RUNNER_TEMP/nightly-e2e-ios-build"
mkdir -p "$build_contents"
tar -xzf "$RUNNER_TEMP/nightly-e2e-ios.tar.gz" -C "$build_contents"
app_path=$(find "$build_contents" -type d -name '*.app' -print -quit)
if [ -z "$app_path" ]; then
echo "The local EAS build did not contain an iOS simulator app" >&2
exit 1
fi
xcrun simctl install "$IOS_UDID" "$app_path" 2>&1 | tee -a artifacts/ios/build.log
- name: Run iOS Maestro suite
run: .github/scripts/run-nightly-e2e.sh ios "$IOS_UDID"
- name: Clean up iOS services and simulator
if: always()
run: .github/scripts/cleanup-nightly-e2e.sh ios "${IOS_UDID:-}"
- name: Upload iOS E2E artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nightly-e2e-ios-${{ github.run_id }}
path: artifacts/ios
if-no-files-found: warn
retention-days: 7
android:
name: Android Maestro E2E
if: github.repository == 'bluesky-social/social-app'
# Linux-x64-32core is a repository-managed runner label.
runs-on: Linux-x64-32core
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Prepare E2E configuration
run: |
mkdir -p artifacts/android
echo "Installing dependencies" > artifacts/android/phase.txt
cp .env.example .env.test
cp google-services.json.example google-services.json
- name: Set up Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: Set up Java 17
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: temurin
java-version: "17"
- name: Install dev-env dependencies
run: pnpm --dir dev-env install --frozen-lockfile 2>&1 | tee artifacts/android/dependencies.log
- name: Compile translations
uses: ./.github/actions/compile-i18n
- name: Install Maestro 2.6.1
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" \
| shasum -a 256 --check
unzip -q "$RUNNER_TEMP/maestro.zip" -d "$RUNNER_TEMP"
echo "$RUNNER_TEMP/maestro/bin" >> "$GITHUB_PATH"
"$RUNNER_TEMP/maestro/bin/maestro" --version | tee artifacts/android/maestro-version.log
test "$("$RUNNER_TEMP/maestro/bin/maestro" --version)" = "$MAESTRO_VERSION"
- name: Install and boot one Android emulator
run: |
echo "Booting Android emulator" > artifacts/android/phase.txt
android_sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/usr/local/lib/android/sdk}}"
sdkmanager_bin="$android_sdk/cmdline-tools/latest/bin/sdkmanager"
avdmanager_bin="$android_sdk/cmdline-tools/latest/bin/avdmanager"
# API 35 emulator images have known stability problems in headless CI
# (see flutter/flutter#153445); the qemu process died deterministically
# on the first native stack-screen push with the API 35 image.
system_image="system-images;android-34;google_apis;x86_64"
if [ ! -x "$sdkmanager_bin" ] || [ ! -x "$avdmanager_bin" ]; then
echo "Android command-line tools were not found under $android_sdk" >&2
find "$android_sdk/cmdline-tools" -maxdepth 3 -type f \( \
-name sdkmanager -o -name avdmanager \
\) -print >&2 || true
exit 1
fi
export ANDROID_HOME="$android_sdk"
export ANDROID_SDK_ROOT="$android_sdk"
export PATH="$android_sdk/platform-tools:$android_sdk/emulator:$PATH"
echo "ANDROID_HOME=$android_sdk" >> "$GITHUB_ENV"
echo "ANDROID_SDK_ROOT=$android_sdk" >> "$GITHUB_ENV"
echo "$android_sdk/platform-tools" >> "$GITHUB_PATH"
echo "$android_sdk/emulator" >> "$GITHUB_PATH"
echo "Using Android SDK at $android_sdk"
yes | "$sdkmanager_bin" --sdk_root="$android_sdk" --licenses >/dev/null || true
"$sdkmanager_bin" --sdk_root="$android_sdk" \
"platform-tools" "emulator" "$system_image"
export ANDROID_AVD_HOME="$RUNNER_TEMP/.android/avd"
mkdir -p "$ANDROID_AVD_HOME"
echo "ANDROID_AVD_HOME=$ANDROID_AVD_HOME" >> "$GITHUB_ENV"
echo no | "$avdmanager_bin" create avd \
--force \
--name nightly-e2e \
--package "$system_image" \
--device pixel_6
# Reduce resolution to lighten the SwiftShader software-rendering
# workload, and raise RAM/cores/heap so the debug RN app has headroom.
# The default 2560MB RAM led to silent qemu crashes mid-flow.
printf 'hw.lcd.width=720\nhw.lcd.height=1600\nhw.lcd.density=280\nhw.ramSize=6144\nhw.cpu.ncore=4\nvm.heapSize=512\n' \
>> "$ANDROID_AVD_HOME/nightly-e2e.avd/config.ini"
if [ -e /dev/kvm ] && [ ! -w /dev/kvm ]; then
sudo chmod 666 /dev/kvm
fi
# Disable the emulator's Vulkan feature so graphics goes through the
# plain GLES SwiftShader translator. gfxstream Vulkan via SwiftShader
# Subzero crashed qemu silently at a deterministic rendering step;
# GLES-only is sufficient since the guest renders with skiagl.
#
# Run the launch in a background subshell so the emulator's exit
# status is recorded when it dies (it is otherwise backgrounded and
# its death is invisible). Write the emulator's real PID - not the
# subshell's - to emulator.pid, since cleanup-nightly-e2e.sh kills the
# PID from that file directly; killing the subshell would not kill the
# emulator child.
(
# wait returns the emulator's non-zero status on crash; set -e would
# abort the subshell before the status is logged.
set +e
"$android_sdk/emulator/emulator" @nightly-e2e \
-port 5554 \
-no-window \
-gpu swiftshader_indirect \
-feature -Vulkan \
-no-snapshot \
-noaudio \
-no-boot-anim \
-camera-back none \
> artifacts/android/emulator.log 2>&1 &
emulator_pid=$!
echo "$emulator_pid" > artifacts/android/emulator.pid
wait "$emulator_pid"
echo "Emulator exited with status $?" >> artifacts/android/emulator.log
) &
adb -s emulator-5554 wait-for-device
booted=false
for _ in $(seq 1 120); do
if [ "$(adb -s emulator-5554 shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; then
booted=true
break
fi
sleep 5
done
if [ "$booted" != "true" ]; then
echo "Android emulator did not finish booting" >&2
exit 1
fi
adb -s emulator-5554 shell settings put global window_animation_scale 0
adb -s emulator-5554 shell settings put global transition_animation_scale 0
adb -s emulator-5554 shell settings put global animator_duration_scale 0
- name: Mark Android development client build phase
run: echo "Building the Android development client" > artifacts/android/phase.txt
- name: Build Android development client
uses: ./.github/actions/eas-local-build
with:
platform: android
profile: e2e
output: ${{ runner.temp }}/nightly-e2e-android.apk
log-path: artifacts/android/build.log
- name: Install Android development client
run: |
adb -s emulator-5554 install -r "$RUNNER_TEMP/nightly-e2e-android.apk" \
2>&1 | tee -a artifacts/android/build.log
- name: Run Android Maestro suite
run: .github/scripts/run-nightly-e2e.sh android emulator-5554
- name: Capture emulator crash diagnostics
if: always()
run: |
{
echo "=== Emulator process status ==="
pgrep -fa "emulator.*nightly-e2e" || echo "Emulator process not found"
echo "=== Emulator exit status ==="
grep "Emulator exited" artifacts/android/emulator.log || echo "No emulator exit status recorded"
echo "=== OOM killer check (kernel) ==="
oom_lines=$(sudo dmesg 2>/dev/null | grep -iE "oom|killed process|out of memory" | tail -20)
echo "${oom_lines:-No kernel OOM evidence found (or dmesg unavailable)}"
echo "=== systemd-oomd check ==="
oomd_lines=$(journalctl -u systemd-oomd --no-pager 2>/dev/null | tail -20)
echo "${oomd_lines:-No systemd-oomd journal entries (or journalctl unavailable)}"
echo "=== journal kernel tail ==="
journalctl -k --no-pager 2>/dev/null | tail -30 || echo "journalctl -k unavailable"
echo "=== Emulator crash database ==="
ls -la /tmp/android-runner/emu-crash-*.db 2>/dev/null || echo "No crash database found"
} > artifacts/android/emulator-diagnostics.log 2>&1
- name: Clean up Android services and emulator
if: always()
run: .github/scripts/cleanup-nightly-e2e.sh android emulator-5554
- name: Upload Android E2E artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nightly-e2e-android-${{ github.run_id }}
path: artifacts/android
if-no-files-found: warn
retention-days: 7
report:
name: Report E2E failures
needs: [ios, android]
if: ${{ always() && github.repository == 'bluesky-social/social-app' }}
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Download iOS artifacts
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: nightly-e2e-ios-${{ github.run_id }}
path: downloaded-artifacts/ios
- name: Download Android artifacts
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: nightly-e2e-android-${{ github.run_id }}
path: downloaded-artifacts/android
- name: Resolve artifact links
env:
GH_TOKEN: ${{ github.token }}
run: |
run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}#artifacts"
jq -n --arg run "$run_url" '{ios: $run, android: $run}' > artifact-links.json
if gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
> artifact-response.json; then
jq --arg base "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts" \
--arg run "$run_url" \
'{
ios: (.artifacts | map(select(.name | startswith("nightly-e2e-ios-"))) | first | if . then ($base + "/" + (.id | tostring)) else $run end),
android: (.artifacts | map(select(.name | startswith("nightly-e2e-android-"))) | first | if . then ($base + "/" + (.id | tostring)) else $run end)
}' artifact-response.json > artifact-links.json
fi
- name: Summarize platform results
id: summary
env:
IOS_STATUS: ${{ needs.ios.result }}
ANDROID_STATUS: ${{ needs.android.result }}
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.json
echo "notify=$(jq -r .notify e2e-summary.json)" >> "$GITHUB_OUTPUT"
echo "payload=$(jq -c .payload e2e-summary.json)" >> "$GITHUB_OUTPUT"
jq -r .githubSummary e2e-summary.json >> "$GITHUB_STEP_SUMMARY"
- name: Notify Slack of E2E failures
if: steps.summary.outputs.notify == 'true'
uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5
with:
webhook: ${{ secrets.E2E_FAILURES_SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: ${{ steps.summary.outputs.payload }}
@@ -21,7 +21,7 @@ jobs:
ssh-key: ${{secrets.GH_ACTION_DEPLOY_KEY}}
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
+1 -1
View File
@@ -132,7 +132,7 @@ jobs:
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
+50 -9
View File
@@ -19,8 +19,47 @@ concurrency:
permissions: {}
jobs:
# Populate this from main so every PR can restore the same trusted baseline.
webpack-analyzer-base:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'push' }}
permissions:
contents: read
steps:
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
- name: ⬇️ Get base stats from cache
id: get-base-stats
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: stats.json
key: stats-base-main-${{ github.sha }}
- name: 🔦 Generate stats file for base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
run: |
pnpm install
pnpm intl:build
pnpm generate-webpack-stats-file
- name: ⬆️ Save base stats to cache
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: stats.json
key: stats-base-main-${{ github.sha }}
webpack-analyzer:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
permissions:
contents: read
@@ -34,7 +73,7 @@ jobs:
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
@@ -71,15 +110,18 @@ jobs:
- name: ⬇️ Get base stats from cache
id: get-base-stats
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
# Restore-only prevents PR-scoped fallback builds from creating caches.
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: stats-base.json
key: stats-base-${{ steps.base-commit.outputs.base-commit }}
path: stats.json
key: stats-base-main-${{ steps.base-commit.outputs.base-commit }}
- name: Restore to base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
env:
BASE_COMMIT: ${{ steps.base-commit.outputs.base-commit }}
run: |
git reset HEAD~
git reset "$BASE_COMMIT"
git restore .
- name: 🔦 Generate stats file from base commit
@@ -88,13 +130,12 @@ jobs:
pnpm install
pnpm intl:build
pnpm generate-webpack-stats-file
mv stats.json stats-base.json
- name: % Get diff
id: get-diff
uses: NejcZdovc/bundle-size-diff@5321de41d2d62a7b0f4d6e60f59d1280a0034160 # v1.1.0
with:
base_path: "stats-base.json"
base_path: "stats.json"
pr_path: "../stats-new.json"
excluded_assets: "(.+).chunk.js|(.+).js.map|(.+).json|(.+).png|(.+).svg|(.+).webp|(.+).jpg|(.+).ico"
@@ -127,7 +168,7 @@ jobs:
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: 🔧 Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
+16
View File
@@ -44,6 +44,12 @@ appId: xyz.blueskyweb.app
id: "e2eRefreshHome"
- tapOn:
id: "replyBtn"
# Wait for the composer to fully open before typing. Tapping replyBtn right
# after the previous publish can race the closing composer on Android.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply text only"
- tapOn:
id: "composerPublishBtn"
@@ -51,6 +57,11 @@ appId: xyz.blueskyweb.app
id: "composeFAB"
- tapOn:
id: "replyBtn"
# Wait for the composer to fully open before typing.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply with an image"
- tapOn:
id: "openMediaBtn"
@@ -63,6 +74,11 @@ appId: xyz.blueskyweb.app
id: "composeFAB"
- tapOn:
id: "replyBtn"
# Wait for the composer to fully open before typing.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply with a https://example.com link card"
- tapOn:
id: "composerPublishBtn"
+81 -44
View File
@@ -29,51 +29,88 @@ appId: xyz.blueskyweb.app
id: "homeScreenFeedTabs-selector-1"
text: "alice-favs"
# Set alice-favs first
- tapOn: "Open drawer menu"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- swipe:
label: "Drag feed down"
from:
id: "feed-drag-handle"
direction: "DOWN"
duration: 1000
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
- assertVisible:
id: "homeScreenFeedTabs-selector-1"
text: "Following"
# Reordering feeds is driven by a drag on the feed-drag-handle. Maestro cannot
# activate the RNGH Pan gesture from a synthetic swipe on Android (proven
# twice - coordinate swipes never register the pan), so the reorder
# verification below runs on iOS only. If Android drag coverage is needed,
# revisit with the SavedFeedsA11y move buttons rather than a swipe.
- runFlow:
when:
platform: iOS
commands:
# Set alice-favs first
- tapOn: "Open drawer menu"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- swipe:
label: "Drag feed down"
from:
id: "feed-drag-handle"
direction: "DOWN"
duration: 1000
- assertVisible:
id: "saveChangesBtn"
enabled: true
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
- assertVisible:
id: "homeScreenFeedTabs-selector-1"
text: "Following"
# Set following first
- tapOn: "Open drawer menu"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- swipe:
label: "Drag feed down"
from:
id: "feed-drag-handle"
direction: "DOWN"
duration: 1000
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "Following"
- assertVisible:
id: "homeScreenFeedTabs-selector-1"
text: "alice-favs"
# Set following first
- tapOn: "Open drawer menu"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- swipe:
label: "Drag feed down"
from:
id: "feed-drag-handle"
direction: "DOWN"
duration: 1000
- assertVisible:
id: "saveChangesBtn"
enabled: true
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "Following"
- assertVisible:
id: "homeScreenFeedTabs-selector-1"
text: "alice-favs"
# On Android, the reorder path above is skipped. Smoke-test that the feeds
# edit screen opens and the pinned feeds render, then return to a valid state.
- runFlow:
when:
platform: Android
commands:
- tapOn: "Open drawer menu"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- assertVisible: "Following"
- assertVisible: "alice-favs"
# Two back presses to reach Home: the first pops the saved-feeds editor
# back to the Feeds screen, the second pops Feeds back to Home. On iOS the
# equivalent path saves changes first (saveChangesBtn calls
# navigation.goBack), so a single "Go back" there already lands on Home.
# This Android smoke branch never saves, so it needs the extra pop to
# leave the screen state on Home, which the shared steps below expect.
- tapOn: "Go back"
- tapOn: "Go back"
# Remove following
- tapOn: "Open drawer menu"
@@ -15,6 +15,24 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "e2eStartOnboarding"
- tapOn: "Open avatar creator"
# The avatar-creator bottom sheet (Dialog.Inner, non-scrollable) opens only
# half-expanded on the short E2E emulator (720x1600), a ~220px sliver with the
# emoji grid below the fold. It is NOT a scroll view, so scrollUntilVisible's
# swipe grabs the sheet's own drag gesture and flings it closed. Instead, drag
# the sheet upward to expand it to full height, which brings the picker into
# view. iOS opens the sheet fully already, so this is Android-only.
- runFlow:
when:
platform: Android
commands:
- swipe:
label: "Drag the bottom sheet up to expand it"
start: "50%, 90%"
end: "50%, 20%"
duration: 600
- extendedWaitUntil:
visible: "Select an emoji"
timeout: 10000
- tapOn: "Select the zap emoji as your avatar"
- tapOn:
label: "Tap on yellow"
@@ -22,6 +40,20 @@ appId: xyz.blueskyweb.app
- tapOn: "Done"
- waitForAnimationToEnd
- tapOn: "Select an avatar"
# Reopening the creator sheet lands on the same half-expanded sliver on
# Android, so expand it again before reaching for the emoji grid. No-op on iOS.
- runFlow:
when:
platform: Android
commands:
- swipe:
label: "Drag the bottom sheet up to expand it"
start: "50%, 90%"
end: "50%, 20%"
duration: 600
- extendedWaitUntil:
visible: "Select an emoji"
timeout: 10000
- tapOn: "Select the atom emoji as your avatar"
- tapOn: "Done"
- waitForAnimationToEnd
+22 -7
View File
@@ -16,13 +16,28 @@ appId: xyz.blueskyweb.app
id: "e2eStartOnboarding"
- tapOn: "Select an avatar"
- waitForAnimationToEnd
- assertVisible: "Photos"
- assertVisible: "Collections"
- tapOn:
point: "50%,22%"
- waitForAnimationToEnd
- tapOn: "Done"
- waitForAnimationToEnd
- runFlow:
when:
platform: iOS
commands:
- assertVisible: "Photos"
- assertVisible: "Collections"
- tapOn:
point: "50%,22%"
- waitForAnimationToEnd
- tapOn: "Done"
- waitForAnimationToEnd
- runFlow:
when:
platform: Android
commands:
# The system photo picker opened here shows MediaStore photos, which
# the e2e run doesn't seed (media is seeded into app-scoped storage for
# the composer's mocked picker instead). With no photo to pick, dismiss
# the picker and continue - onContinue falls back to the generated
# placeholder avatar, and nothing later in the flow depends on the image.
- back
- waitForAnimationToEnd
- tapOn:
id: "onboardingContinue"
- assertVisible: "What are your interests?"
+5 -2
View File
@@ -45,7 +45,7 @@ appId: xyz.blueskyweb.app
id: "editProfileSaveBtn"
- assertNotVisible:
id: "editProfileModal"
- assertVisible: "Alicia"
- assertVisible: ".*Alicia.*"
- assertVisible: "One cool hacker"
# Remove display name and description via the edit profile modal
@@ -64,7 +64,10 @@ appId: xyz.blueskyweb.app
id: "editProfileSaveBtn"
- assertNotVisible:
id: "editProfileModal"
- assertVisible: "alice.test"
# The display-name node renders the handle as a Text with a nested badge View
# once the display name is cleared, so the a11y text is not the bare handle
# string on Android. Match it as a substring instead.
- assertVisible: ".*alice\\.test.*"
- assertNotVisible: "One cool hacker"
# Set avi and banner via the edit profile modal
@@ -22,5 +22,7 @@ appId: xyz.blueskyweb.app
text: "Send report to Dev-env Moderation"
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
+4 -2
View File
@@ -22,5 +22,7 @@ appId: xyz.blueskyweb.app
text: "Send report to Dev-env Moderation"
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
@@ -39,5 +39,7 @@ appId: xyz.blueskyweb.app
text: Your report will be sent to Dev-env Moderation.*
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
@@ -29,5 +29,7 @@ appId: xyz.blueskyweb.app
- hideKeyboard
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
+85 -12
View File
@@ -20,6 +20,12 @@ appId: xyz.blueskyweb.app
- inputText: "Test thread"
- tapOn:
id: "composerPublishBtn"
# Wait for the composer to close and the home feed to settle before signing
# out. Without a settle guard the next action can race the closing composer.
- extendedWaitUntil:
visible:
id: "composeFAB"
timeout: 10000
# Login, reply to the thread, and log out
- tapOn:
@@ -31,9 +37,19 @@ appId: xyz.blueskyweb.app
id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "replyBtn"
# Wait for the composer to fully open before typing.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply 1"
- tapOn:
id: "composerPublishBtn"
# Wait for the composer to close before signing out.
- extendedWaitUntil:
visible:
id: "composeFAB"
timeout: 10000
# Login, confirm notification exists, mute thread, and log out
- tapOn:
@@ -45,10 +61,8 @@ appId: xyz.blueskyweb.app
id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "bottomBarNotificationsBtn"
- assertVisible:
id: "feedItem-by-bob.test"
- tapOn:
id: "feedItem-by-bob.test"
- assertVisible: ".*Reply 1.*"
- tapOn: ".*Reply 1.*"
- tapOn:
id: "postDropdownBtn"
childOf:
@@ -67,16 +81,78 @@ appId: xyz.blueskyweb.app
id: "bottomBarProfileBtn"
- tapOn:
id: "profilePager-selector-1"
- tapOn:
id: "replyBtn"
# Both replies target the thread root ("Test thread" by alice), which sits at
# the top of bob's Replies tab. That tab renders each post in the thread with
# its own replyBtn, so scope the tap to the root post's card
# (feedItem-by-alice.test) rather than relying on which replyBtn Maestro picks
# first. This keeps both reply taps deterministic regardless of list order or
# how many posts have rendered.
#
# Even with the close-gating below, the replyBtn tap can land on a recycled list
# row while the author feed re-renders after a publish, and be swallowed so the
# composer never opens. Wrapping the tap + open-wait in retry makes opening the
# composer idempotent: a swallowed tap just re-taps until the publish button
# appears. A first-try success does not retry.
- retry:
maxRetries: 3
commands:
- tapOn:
id: "replyBtn"
childOf:
id: "feedItem-by-alice.test"
# Wait for the composer to fully open before typing.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply 2"
- tapOn:
id: "composerPublishBtn"
- tapOn:
id: "replyBtn"
# Wait for the composer to actually close before opening it again. replyBtn
# stays in the accessibility tree behind the open composer sheet, so waiting on
# its visibility returns immediately and does not gate on the close animation or
# the author-feed re-render that follows a post - the next replyBtn tap then
# fires mid-transition and is swallowed, so the composer never opens. Gate on
# the publish button disappearing (the composer is gone), then confirm the
# reply button underneath is back and let animations settle.
- extendedWaitUntil:
notVisible:
id: "composerPublishBtn"
timeout: 15000
- extendedWaitUntil:
visible:
id: "replyBtn"
timeout: 10000
- waitForAnimationToEnd
# As with Reply 2: even after gating on the composer close, this tap can hit a
# recycled row during the post-publish feed re-render and be swallowed, so wrap
# the open in retry to make it idempotent.
- retry:
maxRetries: 3
commands:
- tapOn:
id: "replyBtn"
childOf:
id: "feedItem-by-alice.test"
# Wait for the composer to fully open before typing.
- extendedWaitUntil:
visible:
id: "composerPublishBtn"
timeout: 10000
- inputText: "Reply 3"
- tapOn:
id: "composerPublishBtn"
# Wait for the composer to actually close before signing out. As above,
# replyBtn stays visible behind the sheet, so gate on the publish button
# disappearing first, then confirm the reply button underneath has returned.
- extendedWaitUntil:
notVisible:
id: "composerPublishBtn"
timeout: 15000
- extendedWaitUntil:
visible:
id: "replyBtn"
timeout: 10000
# Login, confirm notifications dont exist, unmute the thread, ~~confirm notifications exist~~
# Mute thread behaviour no longer change old notifications after muting/unmuting a thread -sfn
@@ -92,10 +168,7 @@ appId: xyz.blueskyweb.app
- assertVisible: ".*Reply 1.*"
- assertNotVisible: ".*Reply 2.*"
- assertNotVisible: ".*Reply 3.*"
- assertVisible:
id: "feedItem-by-bob.test"
- tapOn:
id: "feedItem-by-bob.test"
- tapOn: ".*Reply 1.*"
- tapOn:
id: "postDropdownBtn"
childOf:
+17 -13
View File
@@ -9,23 +9,27 @@ appId: xyz.blueskyweb.app
when:
platform: iOS
commands:
- openLink: "exp+bluesky://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081"
- runFlow:
when:
visible: 'Open in "Bluesky"'
commands:
- tapOn: Open
- extendedWaitUntil:
visible: "http://localhost:8081"
timeout: 60000
- tapOn: "http://localhost:8081"
- runFlow:
when:
platform: Android
commands:
- tapOn: 'http://localhost:8081'
- runFlow:
label: "Dismiss Expo dev menu"
when:
visible: "Continue"
commands:
- back
- 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}
+1
View File
@@ -328,6 +328,7 @@ func serve(cctx *cli.Context) error {
e.GET("/settings/interests", server.WebGenericNoindex)
e.GET("/settings/about", server.WebGenericNoindex)
e.GET("/settings/notifications", server.WebGenericNoindex)
e.GET("/settings/notifications/activity", server.WebGenericNoindex)
e.GET("/sys/debug", server.WebGenericNoindex)
e.GET("/sys/debug-mod", server.WebGenericNoindex)
e.GET("/sys/log", server.WebGenericNoindex)
+2 -1
View File
@@ -3,7 +3,8 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "NODE_ENV=development ./dev-infra/with-test-redis-and-db.sh node ./mock-server.ts"
"start": "NODE_ENV=development ./dev-infra/with-test-redis-and-db.sh node ./mock-server.ts",
"start:external": "NODE_ENV=development PGPORT=5433 PGHOST=localhost PGUSER=pg PGPASSWORD=password PGDATABASE=postgres DB_POSTGRES_URL=postgresql://pg:password@127.0.0.1:5433/postgres REDIS_HOST=127.0.0.1:6380 node ./mock-server.ts"
},
"dependencies": {
"@atproto/api": "^0.20.22",
+43
View File
@@ -29,6 +29,49 @@ adb reverse tcp:3000 tcp:3000
- In a second tab, run `pnpm e2e:build`
- In a third tab, run `pnpm e2e:run __e2e__`
## Nightly Maestro CI
The `Nightly Maestro E2E` GitHub Actions workflow runs every day at 04:00 UTC
and can also be started from the Actions tab with **Run workflow**. It runs iOS
and Android concurrently, but each platform runs all of `__e2e__/config.yml`
sequentially on one explicitly selected simulator or emulator. The flows share a
stateful mock-server manager, so the suite must not be sharded.
The jobs run Maestro CLI 2.6.1 locally on GitHub Actions; Maestro Cloud is not
used. iOS runs on `macos-26-xlarge` with Xcode 26.4. Android runs on
`Linux-x64-32core`. Both use Java 17 and the Node and pnpm versions declared in
`package.json`. The iOS job selects an iPhone 17 simulator running iOS 26.5;
Android directly provisions and boots a Pixel 6 AVD with the API 35 Google APIs
x86_64 image using the Android SDK command-line tools.
Both development clients use the `e2e` EAS profile and the same reusable local
EAS build action as the release build workflows; the resulting simulator app
and APK are installed directly on the selected devices.
The mock-server manager listens on host port 1986 and creates test services on
port 3000. Metro listens on 8081. Android reverses ports 3000 and 8081 into the
emulator; port 1986 remains host-side because Maestro JavaScript calls it from
the runner. Android uses the existing Docker Compose PostgreSQL 14 and Redis 7
services on ports 5433 and 6380. GitHub-hosted macOS cannot run nested Docker
virtualization, so iOS provisions ephemeral native PostgreSQL 14.x and Redis
7.4.7 on those same ports and starts `pnpm --dir dev-env start:external`.
Each platform uploads a `nightly-e2e-<platform>-<run-id>` artifact for 14 days.
It contains JUnit at `report.xml`, Maestro screenshots, videos, command metadata
and `maestro.log` under `maestro/`, plus Metro, native build, mock-server, service,
dependency, and translation logs. The workflow always uploads what was captured,
including when setup or the native build fails before Maestro starts.
Add the repository secret `E2E_FAILURES_SLACK_WEBHOOK` before enabling the
schedule. The aggregation job runs even when either platform fails and posts one
detailed Slack notification containing both job statuses, failed flow details or
the failed setup phase, the commit and workflow links, and links to both artifact
sets. Successful runs do not post to Slack.
Before relying on the schedule, manually dispatch the workflow and verify both
platforms against live Metro and `dev-env`, Android localhost routing, artifact
uploads on success and failure, one Slack message for a forced failure, and no
Slack message for an all-green run.
## Using Flashlight for Performance Testing
1. Make sure Maestro is installed (optional: only for automated testing) by following the instructions above
2. Install Flashlight by following [these instructions](https://docs.flashlight.dev/)
+11
View File
@@ -21,6 +21,17 @@
"EXPO_PUBLIC_ENV": "production"
}
},
"e2e": {
"extends": "development",
"android": {
"buildType": "apk"
},
"env": {
"EXPO_PUBLIC_ENV": "e2e",
"NODE_ENV": "test",
"RN_SRC_EXT": "e2e.ts,e2e.tsx"
}
},
"preview": {
"extends": "base",
"distribution": "internal",
@@ -146,6 +146,7 @@ function BottomSheetNativeComponentInner({
const insets = useSafeAreaInsets()
const cornerRadius = rest.cornerRadius ?? 0
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
@@ -182,7 +183,7 @@ function BottomSheetNativeComponentInner({
]}>
<View
onLayout={onLayout}
style={maxHeight == null ? undefined : {flex: 1}}>
style={isHeightConstrained ? {flex: 1} : undefined}>
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
</View>
</View>
@@ -1,5 +1,14 @@
import {Component} from 'react'
import {type BottomSheetViewProps} from './BottomSheet.types'
export function BottomSheetNativeComponent(_: BottomSheetViewProps) {
throw new Error('BottomSheetNativeComponent is not available on web')
export class BottomSheetNativeComponent extends Component<BottomSheetViewProps> {
/*
* Native sheets do not exist on web; there is nothing to dismiss.
*/
static dismissAll = async () => {}
render(): never {
throw new Error('BottomSheetNativeComponent is not available on web')
}
}
@@ -6,7 +6,15 @@ export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo
throw new NotImplementedError()
}
export function getReferrerInfo(): ReferrerInfo | null {
/*
* Promise-returning for parity with Android, whose native referrer API only
* exposes a promise.
*/
export function getReferrerInfo(): Promise<ReferrerInfo | null> {
return Promise.resolve(getReferrerInfoSync())
}
function getReferrerInfoSync(): ReferrerInfo | null {
const referrer = SharedPrefs.getString('referrer')
if (referrer) {
SharedPrefs.removeValue('referrer')
@@ -5,6 +5,6 @@ export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo
throw new NotImplementedError()
}
export function getReferrerInfo(): ReferrerInfo | null {
export function getReferrerInfo(): Promise<ReferrerInfo | null> {
throw new NotImplementedError()
}
@@ -7,7 +7,15 @@ export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo
throw new NotImplementedError()
}
export function getReferrerInfo(): ReferrerInfo | null {
/*
* Promise-returning for parity with Android, whose native referrer API only
* exposes a promise.
*/
export function getReferrerInfo(): Promise<ReferrerInfo | null> {
return Promise.resolve(getReferrerInfoSync())
}
function getReferrerInfoSync(): ReferrerInfo | null {
if (
Platform.OS === 'web' &&
// for ssr
-8
View File
@@ -1617,14 +1617,6 @@
"count": 1
}
},
"src/view/com/composer/text-input/TextInput.tsx": {
"typescript/no-floating-promises": {
"count": 1
},
"typescript/no-misused-promises": {
"count": 1
}
},
"src/view/com/composer/text-input/TextInput.web.tsx": {
"typescript/no-misused-promises": {
"count": 1
+9 -5
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.128.0",
"version": "1.129.0",
"private": true,
"engines": {
"node": ">=24.18.0"
@@ -62,7 +62,10 @@
"lint": "oxlint --quiet src modules",
"lint-native": "swiftlint ./modules && ktlint ./modules",
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
"typecheck": "tsgo --project ./tsconfig.check.json",
"typecheck": "pnpm run typecheck:ios && pnpm run typecheck:android && pnpm run typecheck:web",
"typecheck:ios": "tsc --project ./tsconfig.check.ios.json",
"typecheck:android": "tsc --project ./tsconfig.check.android.json",
"typecheck:web": "tsc --project ./tsconfig.check.web.json",
"e2e:mock-server": "cd dev-env && pnpm start",
"e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
"e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
@@ -98,7 +101,7 @@
"@atproto/syntax": "0.7.2",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.14",
"@bsky.app/alf": "^0.1.15",
"@bsky.app/expo-dynamic-app-icon": "^1.8.5",
"@bsky.app/expo-guess-language": "^0.2.8",
"@bsky.app/expo-image-crop-tool": "^0.5.1",
@@ -158,6 +161,7 @@
"expo": "54.0.34",
"expo-age-range": "0.2.18",
"expo-application": "~7.0.8",
"expo-asset": "~12.0.13",
"expo-blur": "~15.0.8",
"expo-build-properties": "~1.0.10",
"expo-camera": "~17.0.10",
@@ -270,7 +274,7 @@
"@types/psl": "1.1.1",
"@types/react": "^19.1.17",
"@types/react-dom": "^19.1.11",
"@typescript/native-preview": "^7.0.0-dev.20260428.1",
"@typescript/native": "npm:typescript@^7.0.2",
"babel-jest": "^29.7.0",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-react-compiler": "19.1.0-rc.3",
@@ -295,7 +299,7 @@
"react-refresh": "^0.14.0",
"svgo": "^4.0.2",
"ts-plugin-sort-import-suggestions": "^1.0.4",
"typescript": "^6.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"webpack-bundle-analyzer": "^4.10.1"
},
"jest": {
+12
View File
@@ -55,3 +55,15 @@ index 06829bd00dbded262e1871aaf6db3ae0cfa9d1b1..1b158185a6d7e907aa18ddc806902fcc
invalidate();
}
diff --git a/lib/typescript/ReactNativeSVG.web.d.ts b/lib/typescript/ReactNativeSVG.web.d.ts
deleted file mode 100644
index e2001975fa97398fb159d2f85f45d067200f575b..0000000000000000000000000000000000000000
diff --git a/lib/typescript/ReactNativeSVG.web.d.ts.map b/lib/typescript/ReactNativeSVG.web.d.ts.map
deleted file mode 100644
index c81af4416af5ee3eaca77b51bfc2952b30d49c8a..0000000000000000000000000000000000000000
diff --git a/lib/typescript/elements.web.d.ts b/lib/typescript/elements.web.d.ts
deleted file mode 100644
index cf4083fbf749b2b002fb649d80b33ac74763847b..0000000000000000000000000000000000000000
diff --git a/lib/typescript/elements.web.d.ts.map b/lib/typescript/elements.web.d.ts.map
deleted file mode 100644
index 29602aa47ad903bc7aaa111462a61dd862c4476d..0000000000000000000000000000000000000000
+219 -78
View File
@@ -231,7 +231,7 @@ patchedDependencies:
react-native-keyboard-controller@1.21.8: ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271
react-native-pager-view@6.8.0: 0979d6fe1e2fa43b2784cc0b5e0ff0117d53b53423e85ee5855eefdc41a00108
react-native-reanimated@3.19.1: 97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6
react-native-svg@15.12.1: 31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9
react-native-svg@15.12.1: ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310
react-native-uitextview@1.4.0: 62de26caadfc39d1bdff204cdc03a705c0cd343999825e06d8c0c120de06cc99
react-native-view-shot@4.0.3: a2457dc30a82cc8c21f371a6f860d9396d450a2a1b4265b1a4ee13bdb24d3268
react-native@0.81.5: 2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194
@@ -257,8 +257,8 @@ importers:
specifier: ^6.0.2
version: 6.0.4
'@bsky.app/alf':
specifier: ^0.1.14
version: 0.1.14(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
specifier: ^0.1.15
version: 0.1.15(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@bsky.app/expo-dynamic-app-icon':
specifier: ^1.8.5
version: 1.8.5(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -330,10 +330,10 @@ importers:
version: 9.2.7
'@lingui/core':
specifier: ^5.9.2
version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))
version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))
'@lingui/react':
specifier: ^5.9.2
version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))(react@19.1.0)
version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))(react@19.1.0)
'@react-native-async-storage/async-storage':
specifier: 2.2.0
version: 2.2.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))
@@ -436,6 +436,9 @@ importers:
expo-application:
specifier: ~7.0.8
version: 7.0.8(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))
expo-asset:
specifier: ~12.0.13
version: 12.0.13(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo-blur:
specifier: ~15.0.8
version: 15.0.8(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -642,10 +645,10 @@ importers:
version: 6.8.0(patch_hash=0979d6fe1e2fa43b2784cc0b5e0ff0117d53b53423e85ee5855eefdc41a00108)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-progress:
specifier: bluesky-social/react-native-progress
version: https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))
version: https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))
react-native-qrcode-styled:
specifier: ^0.3.3
version: 0.3.3(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
version: 0.3.3(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-reanimated:
specifier: 3.19.1
version: 3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -657,7 +660,7 @@ importers:
version: 4.24.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-svg:
specifier: 15.12.1
version: 15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
version: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-uitextview:
specifier: ^1.4.0
version: 1.4.0(patch_hash=62de26caadfc39d1bdff204cdc03a705c0cd343999825e06d8c0c120de06cc99)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -696,7 +699,7 @@ importers:
version: 2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
sonner-native:
specifier: 0.21.0
version: 0.21.0(patch_hash=4bcd0da0fec32e943460e6e788a9e4adf601b507d2dfd0c8246a6c8c74d8f638)(4e42d15c94c2c56af69d343a007eb893)
version: 0.21.0(patch_hash=4bcd0da0fec32e943460e6e788a9e4adf601b507d2dfd0c8246a6c8c74d8f638)(0ed429160839218d22ac63d892397351)
tippy.js:
specifier: ^6.3.7
version: 6.3.7
@@ -727,10 +730,10 @@ importers:
version: 4.14.2
'@lingui/babel-plugin-lingui-macro':
specifier: ^5.9.2
version: 5.9.5(typescript@6.0.3)
version: 5.9.5(@typescript/typescript6@6.0.2)
'@lingui/cli':
specifier: ^5.9.2
version: 5.9.5(typescript@6.0.3)
version: 5.9.5(@typescript/typescript6@6.0.2)
'@pmmmwh/react-refresh-webpack-plugin':
specifier: ^0.5.15
version: 0.5.17(react-refresh@0.14.2)(type-fest@1.4.0)(webpack-dev-server@4.15.2(webpack@5.106.2(postcss@8.5.14)))(webpack@5.106.2(postcss@8.5.14))
@@ -767,9 +770,9 @@ importers:
'@types/react-dom':
specifier: ^19.1.11
version: 19.1.11(@types/react@19.1.17)
'@typescript/native-preview':
specifier: ^7.0.0-dev.20260428.1
version: 7.0.0-dev.20260512.1
'@typescript/native':
specifier: npm:typescript@^7.0.2
version: typescript@7.0.2
babel-jest:
specifier: ^29.7.0
version: 29.7.0(@babel/core@7.29.0)
@@ -846,8 +849,8 @@ importers:
specifier: ^1.0.4
version: 1.0.4
typescript:
specifier: ^6.0.2
version: 6.0.3
specifier: npm:@typescript/typescript6@^6.0.2
version: '@typescript/typescript6@6.0.2'
webpack-bundle-analyzer:
specifier: ^4.10.1
version: 4.10.2
@@ -1603,8 +1606,8 @@ packages:
'@braintree/sanitize-url@6.0.4':
resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==}
'@bsky.app/alf@0.1.14':
resolution: {integrity: sha512-c/KK6avyjEnYzhXDsN0rQaTFSbi8BPg4sJCW8aUk8AtKsO34GTaY6FU1adQ2i8xVfe5UIRu9FJjnBoGpuXj21A==}
'@bsky.app/alf@0.1.15':
resolution: {integrity: sha512-e6blt+oZ2klv+Cp7u11FeZckoILdXOtUZ11ATERgjZgb4zX3y7rt4e2fI03JkJO31wTvXHG8A1LnM96Az+ndJg==}
peerDependencies:
react: '*'
react-native: '*'
@@ -3715,51 +3718,128 @@ packages:
'@types/yargs@17.0.35':
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260512.1':
resolution: {integrity: sha512-l9AJi/TIVMPx5R1c7fxZCSA7eUaHeA0C9Mxdxx/oQJo1K/GtbI3mzYe/SiKNltko1KSdKUmWVhPwxTOS289REg==}
'@typescript/typescript-aix-ppc64@7.0.2':
resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==}
engines: {node: '>=16.20.0'}
cpu: [ppc64]
os: [aix]
'@typescript/typescript-darwin-arm64@7.0.2':
resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [darwin]
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260512.1':
resolution: {integrity: sha512-oABZLQrfB8JN2Ct2CiLK5PyE28Em3sIJlZsAMD45/A2ymtIaa5826dwv8vapE5Wjp54ao0LXxCSuKFm1A8zzCQ==}
'@typescript/typescript-darwin-x64@7.0.2':
resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [darwin]
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260512.1':
resolution: {integrity: sha512-xvbwzpTe+5N6bnBI/t9n4zsGzXxz3V6rVbvDUoJmRLfav5fz+ck0QDkGQGUPrQEEIp0KEzQvx7c+AEZnzdvTQA==}
'@typescript/typescript-freebsd-arm64@7.0.2':
resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [freebsd]
'@typescript/typescript-freebsd-x64@7.0.2':
resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [freebsd]
'@typescript/typescript-linux-arm64@7.0.2':
resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [linux]
'@typescript/native-preview-linux-arm@7.0.0-dev.20260512.1':
resolution: {integrity: sha512-0Hs1Gqa/t9cthoPdqHud1pFGUr9DgJivBTjwquTUh8jt/6PI2bQxoMNZLiN/bhqeDFDTzdxoMBfCaytsTMcXqw==}
'@typescript/typescript-linux-arm@7.0.2':
resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==}
engines: {node: '>=16.20.0'}
cpu: [arm]
os: [linux]
'@typescript/native-preview-linux-x64@7.0.0-dev.20260512.1':
resolution: {integrity: sha512-qr5h6FPo74bN/U+EwRuayBhUbxaji8xzFbIbhMOA2oYSc/qozp5ia2g1+9xGw67MXxPPw/IPT+UGvrNK7K1NeQ==}
'@typescript/typescript-linux-loong64@7.0.2':
resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==}
engines: {node: '>=16.20.0'}
cpu: [loong64]
os: [linux]
'@typescript/typescript-linux-mips64el@7.0.2':
resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==}
engines: {node: '>=16.20.0'}
cpu: [mips64el]
os: [linux]
'@typescript/typescript-linux-ppc64@7.0.2':
resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==}
engines: {node: '>=16.20.0'}
cpu: [ppc64]
os: [linux]
'@typescript/typescript-linux-riscv64@7.0.2':
resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==}
engines: {node: '>=16.20.0'}
cpu: [riscv64]
os: [linux]
'@typescript/typescript-linux-s390x@7.0.2':
resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==}
engines: {node: '>=16.20.0'}
cpu: [s390x]
os: [linux]
'@typescript/typescript-linux-x64@7.0.2':
resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [linux]
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260512.1':
resolution: {integrity: sha512-meNWxhNEfaqos2U0JXvfxWvy4JWrKE9fZepCndDZi+t04X+AIiLYp5s6crWnKP67nzVAzMNgTAc8mu8CnGM+/A==}
'@typescript/typescript-netbsd-arm64@7.0.2':
resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [netbsd]
'@typescript/typescript-netbsd-x64@7.0.2':
resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [netbsd]
'@typescript/typescript-openbsd-arm64@7.0.2':
resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [openbsd]
'@typescript/typescript-openbsd-x64@7.0.2':
resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [openbsd]
'@typescript/typescript-sunos-x64@7.0.2':
resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [sunos]
'@typescript/typescript-win32-arm64@7.0.2':
resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==}
engines: {node: '>=16.20.0'}
cpu: [arm64]
os: [win32]
'@typescript/native-preview-win32-x64@7.0.0-dev.20260512.1':
resolution: {integrity: sha512-Hp6vBnxJSKEEAVWgIoWMmfqkZXCdkhm6XTivrwgRzBwWfiTVe2ZyZ7byWegIKeNnBbffg/K2KvoM8JAHl059GQ==}
'@typescript/typescript-win32-x64@7.0.2':
resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==}
engines: {node: '>=16.20.0'}
cpu: [x64]
os: [win32]
'@typescript/native-preview@7.0.0-dev.20260512.1':
resolution: {integrity: sha512-KIzYPGuxZnyiiYkYrozDT94Af2nwbdLXoY1cgGY66RRa9HSEw13RH9WHg8wA8fZhT4wYzF5uF7WY3hz0QhaxGg==}
engines: {node: '>=16.20.0'}
'@typescript/typescript6@6.0.2':
resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==}
hasBin: true
'@ungap/structured-clone@1.3.1':
@@ -8655,6 +8735,11 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
typescript@7.0.2:
resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
engines: {node: '>=16.20.0'}
hasBin: true
ua-parser-js@0.7.41:
resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==}
hasBin: true
@@ -10069,7 +10154,7 @@ snapshots:
'@braintree/sanitize-url@6.0.4': {}
'@bsky.app/alf@0.1.14(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
'@bsky.app/alf@0.1.15(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
@@ -10902,19 +10987,19 @@ snapshots:
'@lingui/babel-plugin-extract-messages@5.9.5': {}
'@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)':
'@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)':
dependencies:
'@babel/core': 7.29.0
'@babel/runtime': 7.29.2
'@babel/types': 7.29.0
'@lingui/conf': 5.9.5(typescript@6.0.3)
'@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))
'@lingui/conf': 5.9.5(@typescript/typescript6@6.0.2)
'@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))
'@lingui/message-utils': 5.9.5
transitivePeerDependencies:
- supports-color
- typescript
'@lingui/cli@5.9.5(typescript@6.0.3)':
'@lingui/cli@5.9.5(@typescript/typescript6@6.0.2)':
dependencies:
'@babel/core': 7.29.0
'@babel/generator': 7.29.1
@@ -10922,10 +11007,10 @@ snapshots:
'@babel/runtime': 7.29.2
'@babel/types': 7.29.0
'@lingui/babel-plugin-extract-messages': 5.9.5
'@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3)
'@lingui/conf': 5.9.5(typescript@6.0.3)
'@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))
'@lingui/format-po': 5.9.5(typescript@6.0.3)
'@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2)
'@lingui/conf': 5.9.5(@typescript/typescript6@6.0.2)
'@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))
'@lingui/format-po': 5.9.5(@typescript/typescript6@6.0.2)
'@lingui/message-utils': 5.9.5
chokidar: 3.5.1
cli-table: 0.3.11
@@ -10948,26 +11033,26 @@ snapshots:
- supports-color
- typescript
'@lingui/conf@5.9.5(typescript@6.0.3)':
'@lingui/conf@5.9.5(@typescript/typescript6@6.0.2)':
dependencies:
'@babel/runtime': 7.29.2
cosmiconfig: 8.3.6(typescript@6.0.3)
cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2)
jest-validate: 29.7.0
jiti: 2.7.0
picocolors: 1.1.1
transitivePeerDependencies:
- typescript
'@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))':
'@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))':
dependencies:
'@babel/runtime': 7.29.2
'@lingui/message-utils': 5.9.5
optionalDependencies:
'@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3)
'@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2)
'@lingui/format-po@5.9.5(typescript@6.0.3)':
'@lingui/format-po@5.9.5(@typescript/typescript6@6.0.2)':
dependencies:
'@lingui/conf': 5.9.5(typescript@6.0.3)
'@lingui/conf': 5.9.5(@typescript/typescript6@6.0.2)
'@lingui/message-utils': 5.9.5
date-fns: 3.6.0
pofile: 1.1.4
@@ -10979,13 +11064,13 @@ snapshots:
'@messageformat/parser': 5.1.1
js-sha256: 0.10.1
'@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))(react@19.1.0)':
'@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))(react@19.1.0)':
dependencies:
'@babel/runtime': 7.29.2
'@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))
'@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))
react: 19.1.0
optionalDependencies:
'@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3)
'@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2)
'@messageformat/parser@5.1.1':
dependencies:
@@ -12563,36 +12648,69 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260512.1':
'@typescript/typescript-aix-ppc64@7.0.2':
optional: true
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260512.1':
'@typescript/typescript-darwin-arm64@7.0.2':
optional: true
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260512.1':
'@typescript/typescript-darwin-x64@7.0.2':
optional: true
'@typescript/native-preview-linux-arm@7.0.0-dev.20260512.1':
'@typescript/typescript-freebsd-arm64@7.0.2':
optional: true
'@typescript/native-preview-linux-x64@7.0.0-dev.20260512.1':
'@typescript/typescript-freebsd-x64@7.0.2':
optional: true
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260512.1':
'@typescript/typescript-linux-arm64@7.0.2':
optional: true
'@typescript/native-preview-win32-x64@7.0.0-dev.20260512.1':
'@typescript/typescript-linux-arm@7.0.2':
optional: true
'@typescript/native-preview@7.0.0-dev.20260512.1':
optionalDependencies:
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260512.1
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260512.1
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260512.1
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260512.1
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260512.1
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260512.1
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260512.1
'@typescript/typescript-linux-loong64@7.0.2':
optional: true
'@typescript/typescript-linux-mips64el@7.0.2':
optional: true
'@typescript/typescript-linux-ppc64@7.0.2':
optional: true
'@typescript/typescript-linux-riscv64@7.0.2':
optional: true
'@typescript/typescript-linux-s390x@7.0.2':
optional: true
'@typescript/typescript-linux-x64@7.0.2':
optional: true
'@typescript/typescript-netbsd-arm64@7.0.2':
optional: true
'@typescript/typescript-netbsd-x64@7.0.2':
optional: true
'@typescript/typescript-openbsd-arm64@7.0.2':
optional: true
'@typescript/typescript-openbsd-x64@7.0.2':
optional: true
'@typescript/typescript-sunos-x64@7.0.2':
optional: true
'@typescript/typescript-win32-arm64@7.0.2':
optional: true
'@typescript/typescript-win32-x64@7.0.2':
optional: true
'@typescript/typescript6@6.0.2':
dependencies:
'@typescript/old': typescript@6.0.3
'@ungap/structured-clone@1.3.1': {}
@@ -13434,14 +13552,14 @@ snapshots:
core-util-is@1.0.3: {}
cosmiconfig@8.3.6(typescript@6.0.3):
cosmiconfig@8.3.6(@typescript/typescript6@6.0.2):
dependencies:
import-fresh: 3.3.1
js-yaml: 4.1.1
parse-json: 5.2.0
path-type: 4.0.0
optionalDependencies:
typescript: 6.0.3
typescript: '@typescript/typescript6@6.0.2'
create-jest@29.7.0(@types/node@24.12.4):
dependencies:
@@ -17082,18 +17200,18 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
react-native-progress@https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)):
react-native-progress@https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)):
dependencies:
prop-types: 15.8.1
react-native-svg: 15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-qrcode-styled@0.3.3(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
react-native-qrcode-styled@0.3.3(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
qrcode: 1.5.4
react: 19.1.0
react-fast-compare: 3.2.2
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
react-native-svg: 15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -17127,7 +17245,7 @@ snapshots:
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
warn-once: 0.1.1
react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
css-select: 5.2.2
css-tree: 1.1.3
@@ -17677,7 +17795,7 @@ snapshots:
uuid: 8.3.2
websocket-driver: 0.7.4
sonner-native@0.21.0(patch_hash=4bcd0da0fec32e943460e6e788a9e4adf601b507d2dfd0c8246a6c8c74d8f638)(4e42d15c94c2c56af69d343a007eb893):
sonner-native@0.21.0(patch_hash=4bcd0da0fec32e943460e6e788a9e4adf601b507d2dfd0c8246a6c8c74d8f638)(0ed429160839218d22ac63d892397351):
dependencies:
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
@@ -17685,7 +17803,7 @@ snapshots:
react-native-reanimated: 3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-screens: 4.24.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-svg: 15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
@@ -18110,6 +18228,29 @@ snapshots:
typescript@6.0.3: {}
typescript@7.0.2:
optionalDependencies:
'@typescript/typescript-aix-ppc64': 7.0.2
'@typescript/typescript-darwin-arm64': 7.0.2
'@typescript/typescript-darwin-x64': 7.0.2
'@typescript/typescript-freebsd-arm64': 7.0.2
'@typescript/typescript-freebsd-x64': 7.0.2
'@typescript/typescript-linux-arm': 7.0.2
'@typescript/typescript-linux-arm64': 7.0.2
'@typescript/typescript-linux-loong64': 7.0.2
'@typescript/typescript-linux-mips64el': 7.0.2
'@typescript/typescript-linux-ppc64': 7.0.2
'@typescript/typescript-linux-riscv64': 7.0.2
'@typescript/typescript-linux-s390x': 7.0.2
'@typescript/typescript-linux-x64': 7.0.2
'@typescript/typescript-netbsd-arm64': 7.0.2
'@typescript/typescript-netbsd-x64': 7.0.2
'@typescript/typescript-openbsd-arm64': 7.0.2
'@typescript/typescript-openbsd-x64': 7.0.2
'@typescript/typescript-sunos-x64': 7.0.2
'@typescript/typescript-win32-arm64': 7.0.2
'@typescript/typescript-win32-x64': 7.0.2
ua-parser-js@0.7.41: {}
ua-parser-js@1.0.41: {}
+18 -8
View File
@@ -117,6 +117,7 @@ import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings'
import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings'
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings'
import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings'
import {SettingsScreen} from '#/screens/Settings/Settings'
import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
@@ -451,6 +452,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
getComponent={() => NotificationSettingsScreen}
options={{title: title(msg`Notification settings`), requireAuth: true}}
/>
<Stack.Screen
name="ActivityNotificationSettings"
getComponent={() => ActivityNotificationSettingsScreen}
options={{
title: title(msg`Activity notifications`),
requireAuth: true,
}}
/>
<Stack.Screen
name="ContentAndMediaSettings"
getComponent={() => ContentAndMediaSettingsScreen}
@@ -1003,14 +1012,15 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
})
if (IS_WEB) {
const referrerInfo = Referrer.getReferrerInfo()
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
ax.metric('deepLink:referrerReceived', {
to: window.location.href,
referrer: referrerInfo?.referrer,
hostname: referrerInfo?.hostname,
})
}
void Referrer.getReferrerInfo().then(referrerInfo => {
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
ax.metric('deepLink:referrerReceived', {
to: window.location.href,
referrer: referrerInfo?.referrer,
hostname: referrerInfo?.hostname,
})
}
})
}
// temp, just testing
+1 -1
View File
@@ -286,7 +286,7 @@ export function Composer({
ref={IS_WEB ? sift.refs.setAnchor : undefined}
style={
node.type === 'facet' && {
color: t.palette.primary_500,
color: t.atoms.text_link.color,
}
}>
{node.raw}
+25 -2
View File
@@ -2,8 +2,9 @@ import {type StyleProp, type ViewStyle} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
import * as Menu from '#/components/Menu'
import {type TriggerProps as MenuTriggerProps} from '#/components/Menu/types'
import {Text} from '#/components/Typography'
import {type AuxiliaryViewProps} from './types'
import {type AuxiliaryViewProps, type TriggerProps} from './types'
export {
ContainerItem,
@@ -16,7 +17,6 @@ export {
ItemText,
LabelText,
Root,
Trigger,
useMenuContext as useContextMenuContext,
useMenuControl as useContextMenuControl,
} from '#/components/Menu'
@@ -30,14 +30,37 @@ export function AuxiliaryView({}: AuxiliaryViewProps) {
return null
}
/*
* On web the context menu is just a Menu; contentLabel, onTap, style, and
* swipeGesture only apply to the native press-and-hold presentation.
*/
export function Trigger({children, label, hint, role}: TriggerProps) {
return (
<Menu.Trigger label={label} hint={hint} role={role}>
{/*
* Menu supplies the same web-arm child props shape as ContextMenu's
* TriggerChildProps; only the native arms of the two unions differ,
* and those never occur here.
*/}
{children as unknown as MenuTriggerProps['children']}
</Menu.Trigger>
)
}
export function Outer({
children,
label,
align: _align,
style,
onCloseAutoFocus,
}: {
children: React.ReactNode
label?: string
/**
* Native positions the menu against the message bubble explicitly; the web
* dropdown is anchored by radix, so this is accepted only for parity.
*/
align?: 'left' | 'right'
style?: StyleProp<ViewStyle>
onCloseAutoFocus?: (event: Event) => void
}) {
+2 -1
View File
@@ -157,7 +157,8 @@ export function Outer({
[open, close],
)
const isHeightConstrained = nativeOptions?.maxHeight != null
const isHeightConstrained =
nativeOptions?.maxHeight != null || nativeOptions?.fullHeight === true
const context = useMemo(
() => ({
+32 -6
View File
@@ -12,6 +12,7 @@ import {
type GestureResponderEvent,
type LayoutChangeEvent,
Pressable,
type ScrollView,
type StyleProp,
View,
type ViewStyle,
@@ -24,6 +25,7 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar'
import {logger} from '#/logger'
import {useA11y} from '#/state/a11y'
import {useDialogStateControlContext} from '#/state/dialogs'
import {type ListMethods} from '#/view/com/util/List'
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {Context} from '#/components/Dialog/context'
@@ -224,11 +226,21 @@ export function Inner({
)
}
export const ScrollableInner = Inner
/*
* There is no inner ScrollView on web - the ref is accepted for parity with
* the native variant and never attached, so native-only scrolling code in
* shared callers stays a no-op here.
*/
export function ScrollableInner({
ref: _ref,
...props
}: DialogInnerProps & {ref?: React.Ref<ScrollView>}) {
return <Inner {...props} />
}
export const InnerFlatList = forwardRef<
FlatList,
FlatListProps<any> & {label: string} & {
ListMethods,
FlatListProps<any> & {label?: string} & {
webInnerStyle?: StyleProp<ViewStyle>
webInnerContentContainerStyle?: StyleProp<ViewStyle>
footer?: React.ReactNode
@@ -247,7 +259,11 @@ export const InnerFlatList = forwardRef<
const {gtMobile} = useBreakpoints()
return (
<Inner
label={label}
/*
* Most shared callers cannot pass a label since the native variant has
* no such prop; aria-label is simply absent for them, as before.
*/
label={label as string}
style={[
a.overflow_hidden,
a.px_0,
@@ -256,7 +272,13 @@ export const InnerFlatList = forwardRef<
]}
contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}>
<FlatList
ref={ref}
/*
* The FlatList instance satisfies the (web) ListMethods interface
* shared callers hold their refs as, except scrollToTop, which no
* platform-agnostic caller can use since the native ListMethods
* (FlatList) lacks it too.
*/
ref={ref as React.Ref<FlatList>}
style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, style]}
{...props}
/>
@@ -321,7 +343,11 @@ export function Close() {
)
}
export function Handle() {
/*
* The drag handle only exists on the native bottom sheet; props are accepted
* for parity with the native variant.
*/
export function Handle(_props: {difference?: boolean; fill?: string}) {
return null
}
@@ -1,5 +1,7 @@
import {useState} from 'react'
import {View} from 'react-native'
import type Animated from 'react-native-reanimated'
import {type AnimatedRef, type SharedValue} from 'react-native-reanimated'
import {useTheme} from '#/alf'
import {DotGrid2x3_Stroke2_Corner0_Rounded as GripIcon} from '#/components/icons/DotGrid'
@@ -18,6 +20,10 @@ interface SortableListProps<T> {
onDragEnd?: () => void
/** Fixed row height used for position math. */
itemHeight: number
/** Ref to the parent Animated.ScrollView for auto-scroll. Ignored on web. */
scrollRef?: AnimatedRef<Animated.ScrollView>
/** Scroll offset shared value from useScrollViewOffset. Ignored on web. */
scrollOffset?: SharedValue<number>
}
export function SortableList<T>({
+2 -2
View File
@@ -481,11 +481,11 @@ export function ProfileGrid({
<Text
style={[
a.text_sm,
{color: t.palette.primary_500},
t.atoms.text_link,
hovered &&
web({
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
textDecorationColor: t.atoms.text_link.color,
}),
]}>
<Trans>See more</Trans>
+4 -3
View File
@@ -4,7 +4,6 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {HITSLOP_30} from '#/lib/constants'
import {type NavigationProp} from '#/lib/routes/types'
import {useSetDrawerOpen} from '#/state/shell'
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
@@ -33,6 +32,8 @@ import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_IOS} from '#/env'
const HEADER_BUTTON_HITSLOP = {top: 30, bottom: 10, left: 30, right: 30}
export function Outer({
children,
noBottomBorder,
@@ -136,7 +137,7 @@ export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
color="secondary"
shape="round"
onPress={onPressBack}
hitSlop={HITSLOP_30}
hitSlop={HEADER_BUTTON_HITSLOP}
style={[
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
a.bg_transparent,
@@ -170,7 +171,7 @@ export function MenuButton() {
color="secondary"
shape="square"
onPress={onPress}
hitSlop={HITSLOP_30}
hitSlop={HEADER_BUTTON_HITSLOP}
style={[
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
a.bg_transparent,
@@ -78,6 +78,7 @@ export function ImageMenu({onPressShare, onPressSave}: Props) {
visible={isMounted}
animationType="none"
onRequestClose={close}
supportedOrientations={['portrait', 'landscape']}
statusBarTranslucent>
<Pressable
accessibilityRole="button"
+5 -5
View File
@@ -415,7 +415,7 @@ function LinkPeek({
// dialog can show.
useInAppBrowser: useInAppBrowserPref === true,
browserToolbarColor: t.atoms.bg.backgroundColor,
browserControlsColor: t.palette.primary_500,
browserControlsColor: t.atoms.text_link.color,
}}
borderRadius={borderRadius}
// Fires only when not morphing natively (in-app browser off/unset).
@@ -487,14 +487,14 @@ export function InlineLinkText({
accessibilityLabel={label}
{...rest}
style={[
{color: t.palette.primary_500},
t.atoms.text_link,
interacted &&
!disableUnderline && {
...web({
outline: 0,
textDecorationLine: 'underline',
textDecorationColor:
flattenedStyle.color ?? t.palette.primary_500,
flattenedStyle.color ?? t.atoms.text_link.color,
}),
},
flattenedStyle,
@@ -591,14 +591,14 @@ export function SimpleInlineLinkText({
accessibilityLabel={label}
{...rest}
style={[
{color: t.palette.primary_500},
t.atoms.text_link,
interacted &&
!disableUnderline && {
...web({
outline: 0,
textDecorationLine: 'underline',
textDecorationColor:
flattenedStyle.color ?? t.palette.primary_500,
flattenedStyle.color ?? t.atoms.text_link.color,
}),
},
flattenedStyle,
+2
View File
@@ -32,6 +32,8 @@ import {
import {Portal} from '#/components/Portal'
import {Text} from '#/components/Typography'
export {type DialogControlProps as MenuControlProps} from '#/components/Dialog'
export {useMenuContext}
export function useMenuControl(): Dialog.DialogControlProps {
+2 -3
View File
@@ -115,6 +115,7 @@ export function ImageEmbed({
onPress(0, [singleContainerRef.current], [singleDimsRef.current])
}
}
console.log(rest.viewContext, 'rest.viewContext')
return (
<View style={[a.mt_sm, rest.style]}>
<ImageContextMenu
@@ -127,9 +128,7 @@ export function ImageEmbed({
crop={
rest.viewContext === PostEmbedViewContext.ThreadHighlighted
? 'none'
: rest.isWithinQuote
? 'square'
: 'constrained'
: 'constrained'
}
image={image}
onContainerRef={ref => {
@@ -1,3 +1,10 @@
export function VideoEmbedInnerWeb() {
import {type VideoEmbedInnerWebProps} from './VideoEmbedInnerWeb.shared'
export {
HLSUnsupportedError,
VideoNotFoundError,
} from './VideoEmbedInnerWeb.shared'
export function VideoEmbedInnerWeb(_props: VideoEmbedInnerWebProps): never {
throw new Error('VideoEmbedInnerWeb may not be used on native.')
}
@@ -0,0 +1,21 @@
import {type AppBskyEmbedVideo} from '@atproto/api'
export type VideoEmbedInnerWebProps = {
embed: AppBskyEmbedVideo.View
active: boolean
setActive: () => void
onScreen: boolean
lastKnownTime: React.RefObject<number | undefined>
}
export class HLSUnsupportedError extends Error {
constructor() {
super('HLS is not supported')
}
}
export class VideoNotFoundError extends Error {
constructor() {
super('Video not found')
}
}
@@ -1,6 +1,5 @@
import {useCallback, useEffect, useId, useRef, useState} from 'react'
import {View} from 'react-native'
import {type AppBskyEmbedVideo} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import type * as HlsTypes from 'hls.js'
@@ -10,21 +9,25 @@ import {atoms as a} from '#/alf'
import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog'
import {useFullscreen} from '#/components/hooks/useFullscreen'
import * as BandwidthEstimate from './bandwidth-estimate'
import {
HLSUnsupportedError,
type VideoEmbedInnerWebProps,
VideoNotFoundError,
} from './VideoEmbedInnerWeb.shared'
import {Controls} from './web-controls/VideoControls'
export {
HLSUnsupportedError,
VideoNotFoundError,
} from './VideoEmbedInnerWeb.shared'
export function VideoEmbedInnerWeb({
embed,
active,
setActive,
onScreen,
lastKnownTime,
}: {
embed: AppBskyEmbedVideo.View
active: boolean
setActive: () => void
onScreen: boolean
lastKnownTime: React.RefObject<number | undefined>
}) {
}: VideoEmbedInnerWebProps) {
const containerRef = useRef<HTMLDivElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
const [focused, setFocused] = useState(false)
@@ -104,12 +107,6 @@ export function VideoEmbedInnerWeb({
)
}
export class HLSUnsupportedError extends Error {
constructor() {
super('HLS is not supported')
}
}
// Bluesky serves HLS as MPEG-TS with H.264 + AAC. `Hls.isSupported()` is loose
// (true if MSE supports *any* of {H.264, AV1, VP9} OR *any* of {AAC, FLAC}),
// so on Linux boxes missing H.264 (e.g. no ubuntu-restricted-extras, sandboxed
@@ -138,12 +135,6 @@ function canPlayBskyVideoCodecs(): boolean {
)
}
export class VideoNotFoundError extends Error {
constructor() {
super('Video not found')
}
}
/**
* Fatal hls.js playback error. `detail` is the hls.js error details code
* (e.g. bufferAppendError), which buckets failures more usefully than the
@@ -1,3 +1,5 @@
export function Controls() {
import {type ControlsProps} from './VideoControls.shared'
export function Controls(_props: ControlsProps): never {
throw new Error('VideoWebControls may not be used on native.')
}
@@ -0,0 +1,17 @@
import type Hls from 'hls.js'
export type ControlsProps = {
videoRef: React.RefObject<HTMLVideoElement | null>
hlsRef: React.RefObject<Hls | undefined | null>
active: boolean
setActive: () => void
focused: boolean
setFocused: (focused: boolean) => void
onScreen: boolean
fullscreenRef: React.RefObject<HTMLDivElement | null>
hlsLoading: boolean
hasSubtitleTrack: boolean
isGif: boolean
altText?: string
updateCuePositions: (controlsVisible?: boolean) => void
}
@@ -3,7 +3,6 @@ import {Pressable, View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import type Hls from 'hls.js'
import {clamp} from '#/lib/numbers'
import {
@@ -33,6 +32,7 @@ import {TimeIndicator} from '../TimeIndicator'
import {ControlButton} from './ControlButton'
import {Scrubber} from './Scrubber'
import {formatTime, useVideoElement} from './utils'
import {type ControlsProps} from './VideoControls.shared'
import {VolumeControl} from './VolumeControl'
export function Controls({
@@ -49,21 +49,7 @@ export function Controls({
isGif,
altText,
updateCuePositions,
}: {
videoRef: React.RefObject<HTMLVideoElement | null>
hlsRef: React.RefObject<Hls | undefined | null>
active: boolean
setActive: () => void
focused: boolean
setFocused: (focused: boolean) => void
onScreen: boolean
fullscreenRef: React.RefObject<HTMLDivElement | null>
hlsLoading: boolean
hasSubtitleTrack: boolean
isGif: boolean
altText?: string
updateCuePositions: (controlsVisible?: boolean) => void
}) {
}: ControlsProps) {
const {
play,
pause,
-1
View File
@@ -4,7 +4,6 @@ import {type AppBskyFeedDefs, type ModerationDecision} from '@atproto/api'
export enum PostEmbedViewContext {
ThreadHighlighted = 'ThreadHighlighted',
Feed = 'Feed',
FeedEmbedRecordWithMedia = 'FeedEmbedRecordWithMedia',
ChatMessage = 'ChatMessage',
}
+5 -7
View File
@@ -1,8 +1,6 @@
import {useCallback, useMemo} from 'react'
import {LayoutAnimation, type TextStyle} 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 {HITSLOP_10} from '#/lib/constants'
import {atoms as a, flatten, type TextStyleProp, useTheme} from '#/alf'
@@ -14,7 +12,7 @@ export function ShowMoreTextButton({
style,
}: TextStyleProp & {onPress: () => void}) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const onPress = useCallback(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
@@ -30,7 +28,7 @@ export function ShowMoreTextButton({
return (
<Button
label={_(msg`Expand post text`)}
label={l`Expand post text`}
onPress={onPress}
style={[
a.self_start,
@@ -43,13 +41,13 @@ export function ShowMoreTextButton({
<Text
style={[
textStyle,
t.atoms.text_link,
{
color: t.palette.primary_500,
opacity: pressed ? 0.6 : 1,
textDecorationLine: hovered ? 'underline' : undefined,
},
]}>
<Trans>Show More</Trans>
<Trans>Show more</Trans>
</Text>
)}
</Button>
+4 -4
View File
@@ -150,10 +150,10 @@ function TranslationLink({
label={l`Translate`}
hoverStyle={[
native({opacity: 0.5}),
web([a.underline, {textDecorationColor: t.palette.primary_500}]),
web([a.underline, {textDecorationColor: t.atoms.text_link.color}]),
]}
hitSlop={HITSLOP_30}>
<Text style={[a.text_sm, {color: t.palette.primary_500}]}>
<Text style={[a.text_sm, t.atoms.text_link]}>
<Trans>Translate</Trans>
</Text>
</Link>
@@ -229,7 +229,7 @@ function TranslationError({
label={l`Try Google Translate`}
hoverStyle={[
native({opacity: 0.5}),
web([a.underline, {textDecorationColor: t.palette.primary_500}]),
web([a.underline, {textDecorationColor: t.atoms.text_link.color}]),
]}
hitSlop={HITSLOP_30}>
<Text
@@ -237,7 +237,7 @@ function TranslationError({
a.text_xs,
a.font_medium,
a.leading_snug,
{color: t.palette.primary_500},
t.atoms.text_link,
]}>
<Trans>Try Google Translate</Trans>
</Text>
@@ -23,7 +23,7 @@ import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {Default as ProfileCard} from '#/components/ProfileCard'
import {IS_NATIVE, IS_WEB} from '#/env'
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) {
function keyExtractor(item: AppBskyActorDefs.ProfileView, index: number) {
return `${item.did}-${index}`
}
@@ -87,7 +87,7 @@ export const ProfilesList = forwardRef<SectionRef, ProfilesListProps>(
const renderItem = ({
item,
index,
}: ListRenderItemInfo<AppBskyActorDefs.ProfileViewBasic>) => {
}: ListRenderItemInfo<AppBskyActorDefs.ProfileView>) => {
return (
<View
style={[
@@ -1,6 +1,5 @@
import {useCallback, useEffect, useImperativeHandle, useState} from 'react'
import {
findNodeHandle,
type ListRenderItemInfo,
type StyleProp,
useWindowDimensions,
@@ -26,6 +25,7 @@ import {
type EmptyStateButtonProps,
} from '#/view/com/util/EmptyState'
import {List, type ListRef} from '#/view/com/util/List'
import {findListNativeTag} from '#/view/com/util/listNativeTag'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {atoms as a, ios, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -57,7 +57,7 @@ interface ProfileFeedgensProps {
emptyStateIcon?: React.ComponentType<any> | React.ReactElement
}
function keyExtractor(item: AppBskyGraphDefs.StarterPackView) {
function keyExtractor(item: AppBskyGraphDefs.StarterPackViewBasic) {
return item.uri
}
@@ -138,13 +138,16 @@ export function ProfileStarterPacks({
useEffect(() => {
if (IS_IOS && enabled && scrollElRef.current) {
const nativeTag = findNodeHandle(scrollElRef.current)
const nativeTag = findListNativeTag(scrollElRef.current)
setScrollViewTag(nativeTag)
}
}, [enabled, scrollElRef, setScrollViewTag])
const renderItem = useCallback(
({item, index}: ListRenderItemInfo<AppBskyGraphDefs.StarterPackView>) => {
({
item,
index,
}: ListRenderItemInfo<AppBskyGraphDefs.StarterPackViewBasic>) => {
return (
<View
style={[
+2 -5
View File
@@ -10,6 +10,7 @@ import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, flatten, useBreakpoints, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {type WelcomeModalControl} from '#/components/hooks/useWelcomeModal.shared'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
@@ -17,11 +18,7 @@ import {useAnalytics} from '#/analytics'
const welcomeModalBg = require('../../assets/images/welcome-modal-bg.jpg')
interface WelcomeModalProps {
control: {
isOpen: boolean
open: () => void
close: () => void
}
control: WelcomeModalControl
}
export function WelcomeModal({control}: WelcomeModalProps) {
@@ -1,3 +1,11 @@
export function FindContactsFlow() {
import {type Action, type State} from './state'
export function FindContactsFlow(_props: {
state: State
dispatch: React.ActionDispatch<[Action]>
onBack?: () => void
onCancel: () => void
context: 'Onboarding' | 'Standalone'
}): never {
throw new Error('FindContactsFlow is not available on web')
}
@@ -1,3 +1,5 @@
export function useWelcomeModal() {
import {type WelcomeModalControl} from './useWelcomeModal.shared'
export function useWelcomeModal(): WelcomeModalControl {
throw new Error('useWelcomeModal is web only')
}
@@ -0,0 +1,5 @@
export type WelcomeModalControl = {
isOpen: boolean
open: () => void
close: () => void
}
+2 -1
View File
@@ -2,8 +2,9 @@ import {useEffect, useState} from 'react'
import {useSession} from '#/state/session'
import {IS_WEB} from '#/env'
import {type WelcomeModalControl} from './useWelcomeModal.shared'
export function useWelcomeModal() {
export function useWelcomeModal(): WelcomeModalControl {
const {hasSession} = useSession()
const [isOpen, setIsOpen] = useState(false)
+2 -4
View File
@@ -5,7 +5,7 @@ import {type AppBskyEmbedImages} from '@atproto/api'
import {atoms as a, useBreakpoints} from '#/alf'
import {type Dimensions} from '#/components/Lightbox/types'
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
import {type PostEmbedViewContext} from '#/components/Post/Embed/types'
import {GalleryItem} from './ImageLayoutGridItem'
interface ImageLayoutGridProps {
@@ -28,9 +28,7 @@ export function ImageLayoutGrid({
...props
}: ImageLayoutGridProps) {
const {gtMobile} = useBreakpoints()
const isWithinQuote =
isWithinQuoteProp ??
props.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
const isWithinQuote = isWithinQuoteProp
const gap = isWithinQuote ? (gtMobile ? a.gap_xs : a.gap_2xs) : a.gap_xs
return (
+10 -17
View File
@@ -6,9 +6,7 @@ import {
type ViewStyle,
} from 'react-native'
import {type ModerationUI} from '@atproto/api'
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 {
ADULT_CONTENT_LABELS,
@@ -78,7 +76,7 @@ function ContentHiderActive({
children?: React.ReactNode
}) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const {gtMobile} = useBreakpoints()
const [override, setOverride] = useState(false)
const control = useModerationDetailsDialogControl()
@@ -97,7 +95,7 @@ function ContentHiderActive({
(blur.type === 'label' && blur.source.type !== 'user')
) {
if (desc.isSubjectAccount) {
return _(msg`${desc.name} (Account)`)
return l`${desc.name} (Account)`
} else {
return desc.name
}
@@ -128,7 +126,7 @@ function ContentHiderActive({
const def = cause.labelDef || getDefinition(labelDefs, cause.label)
if (def.identifier === 'porn' || def.identifier === 'sexual') {
return _(msg`Adult Content`)
return l`Adult Content`
}
return getLabelStrings(i18n.locale, globalLabelStrings, def).name
})
@@ -138,7 +136,7 @@ function ContentHiderActive({
}
return [...new Set(selfBlurNames)].join(', ')
}, [
_,
l,
modui.blurs,
blur,
desc.name,
@@ -151,7 +149,6 @@ function ContentHiderActive({
return (
<View testID={testID} style={[a.overflow_hidden, style]}>
<ModerationDetailsDialog control={control} modcause={blur} />
<Button
onPress={e => {
e.preventDefault()
@@ -166,10 +163,10 @@ function ContentHiderActive({
label={desc.name}
accessibilityHint={
modui.noOverride
? _(msg`Learn more about the moderation applied to this content`)
? l`Learn more about the moderation applied to this content`
: override
? _(msg`Hides the content`)
: _(msg`Shows the content`)
? l`Hides the content`
: l`Shows the content`
}>
{state => (
<View
@@ -223,7 +220,6 @@ function ContentHiderActive({
</View>
)}
</Button>
{desc.source && blur.type === 'label' && !override && (
<Button
onPress={e => {
@@ -231,9 +227,7 @@ function ContentHiderActive({
e.stopPropagation()
control.open()
}}
label={_(
msg`Learn more about the moderation applied to this content`,
)}
label={l`Learn more about the moderation applied to this content`}
style={[a.pt_sm]}>
{state => (
<Text
@@ -252,7 +246,7 @@ function ContentHiderActive({
)}{' '}
<Text
style={[
{color: t.palette.primary_500},
t.atoms.text_link,
a.text_sm,
state.hovered && [web({textDecoration: 'underline'})],
]}>
@@ -262,7 +256,6 @@ function ContentHiderActive({
)}
</Button>
)}
{override && <View style={childContainerStyle}>{children}</View>}
</View>
)
+9 -1
View File
@@ -54,7 +54,15 @@ export function PostAlerts({
const isOwnPost = !!post && post.author.did === currentAccount?.did
const allLabels: ComAtprotoLabelDefs.Label[] =
isOwnPost && view === 'expanded'
? [...(post.labels ?? []), ...(post.author.labels ?? [])]
? [
...(post.labels ?? []),
/*
* Account labels appear on Profile. We don't show them here unless the
* user's mod settings are configured such that the labels land in the
* modui handling.
*/
// ...(post.author.labels ?? [])
]
: []
/*
* Labels that the moderation system already surfaces in this context -
+6 -10
View File
@@ -6,9 +6,7 @@ import {
type ViewStyle,
} from 'react-native'
import {type ModerationUI} from '@atproto/api'
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 {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
@@ -38,7 +36,7 @@ export function ScreenHider({
containerStyle?: StyleProp<ViewStyle>
}>) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const [override, setOverride] = useState(false)
const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries()
@@ -131,15 +129,13 @@ export function ScreenHider({
control.open()
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityLabel={l`Learn more about this warning`}
accessibilityHint="">
<Text
style={[
a.text_lg,
a.leading_snug,
{
color: t.palette.primary_500,
},
t.atoms.text_link,
web({
cursor: 'pointer',
}),
@@ -158,7 +154,7 @@ export function ScreenHider({
color="primary"
size="large"
style={[a.rounded_full]}
label={_(msg`Go back`)}
label={l`Go back`}
onPress={() => {
if (navigation.canGoBack()) {
navigation.goBack()
@@ -176,7 +172,7 @@ export function ScreenHider({
color="secondary"
size="large"
style={[a.rounded_full]}
label={_(msg`Show anyway`)}
label={l`Show anyway`}
onPress={() => setOverride(v => !v)}>
<ButtonText>
<Trans>Show anyway</Trans>
@@ -0,0 +1,15 @@
import {type ColorValue} from 'react-native'
export interface GestureAction {
color: ColorValue
action: () => void
threshold: number
icon: React.ElementType
}
export interface GestureActions {
leftFirst?: GestureAction
leftSecond?: GestureAction
rightFirst?: GestureAction
rightSecond?: GestureAction
}
@@ -16,20 +16,7 @@ import Animated, {
} from 'react-native-reanimated'
import {useHaptics} from '#/lib/haptics'
interface GestureAction {
color: ColorValue
action: () => void
threshold: number
icon: React.ElementType
}
interface GestureActions {
leftFirst?: GestureAction
leftSecond?: GestureAction
rightFirst?: GestureAction
rightSecond?: GestureAction
}
import {type GestureActions} from './GestureActionView.shared'
const MAX_WIDTH = Dimensions.get('screen').width
const ICON_SIZE = 32
@@ -1,3 +1,13 @@
export function GestureActionView({children}: {children: React.ReactNode}) {
import {type GestureActions} from './GestureActionView.shared'
/*
* Swipe gestures only exist on native; children render as-is on web.
*/
export function GestureActionView({
children,
}: {
children: React.ReactNode
actions: GestureActions
}) {
return children
}
+1 -1
View File
@@ -40,7 +40,7 @@ export function useIntentHandler() {
await WebBrowser.dismissBrowser().catch(() => {})
}
const referrerInfo = Referrer.getReferrerInfo()
const referrerInfo = await Referrer.getReferrerInfo()
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
ax.metric('deepLink:referrerReceived', {
to: url,
@@ -8,6 +8,8 @@ export function useNavigationTabState() {
return {
isAtHome: currentRoute === 'Home',
isAtSearch: currentRoute === 'Search',
isAtFeeds: currentRoute === 'Feeds',
isAtBookmarks: currentRoute === 'Bookmarks',
isAtNotifications: currentRoute === 'Notifications',
isAtMyProfile: currentRoute === 'MyProfile',
isAtMessages: currentRoute === 'Messages',
+1 -1
View File
@@ -1,7 +1,7 @@
export function useOTAUpdates() {}
export function useApplyPullRequestOTAUpdate() {
return {
tryApplyUpdate: () => {},
tryApplyUpdate: async (_channel: string) => {},
revertToEmbedded: () => {},
isCurrentlyRunningPullRequestDeployment: false,
currentChannel: 'web-build',
+1 -1
View File
@@ -4,7 +4,7 @@ export function Provider({children}: {children: React.ReactNode}) {
return children
}
const noop = () => {}
const noop = (_scope: string) => {}
export function useHotkeysContext() {
return useMemo(
+6 -2
View File
@@ -168,10 +168,14 @@ function createResizedImage(
export async function saveBytesToDisk(
filename: string,
bytes: Uint8Array<ArrayBuffer>,
bytes: Uint8Array,
type: string,
) {
const blob = new Blob([bytes], {type})
/*
* Bytes handed to us are never SharedArrayBuffer-backed, but the broader
* Uint8Array parameter type matches the native variant.
*/
const blob = new Blob([bytes as Uint8Array<ArrayBuffer>], {type})
const url = URL.createObjectURL(blob)
downloadUrl(url, filename)
// Firefox requires a small delay
+44
View File
@@ -1,3 +1,4 @@
import {Asset} from 'expo-asset'
import {
documentDirectory,
getInfoAsync,
@@ -9,10 +10,15 @@ import ExpoImageCropTool, {
} from '@bsky.app/expo-image-crop-tool'
import {IMAGE_SIZE_CONFIG_2K_1MB} from '#/lib/constants'
import {IS_ANDROID} from '#/env'
import {compressIfNeeded} from './manip'
import {type PickerImage} from './picker.shared'
async function getFile() {
if (IS_ANDROID) {
return await getAndroidFile()
}
const imagesDir = documentDirectory!
.split('/')
.slice(0, -6)
@@ -41,6 +47,44 @@ async function getFile() {
)
}
/*
* The Android emulator can't reach the iOS simulator's sample photo library,
* so we load a jpg bundled with the app instead. It is bundled via require()
* (resolved by Metro), so it survives `pm clear`, which Maestro's clearState
* runs at the start of every flow. An adb-seeded file in app-scoped external
* storage does not survive: pm clear wipes that directory each flow, so the
* seeded file is gone before the picker mock ever reads it.
*/
async function getAndroidFile() {
const asset = Asset.fromModule(
require('../../../assets/images/welcome-modal-bg.jpg'),
)
await asset.downloadAsync()
const path = asset.localUri!
const fileInfo = await getInfoAsync(path)
if (!fileInfo.exists) {
throw new Error('Failed to get file info')
}
/*
* Dimensions of the bundled asset (assets/images/welcome-modal-bg.jpg). Only
* used for downstream aspect-ratio display; the actual bytes are read from
* disk by compressIfNeeded.
*/
return await compressIfNeeded(
{
path,
mime: 'image/jpeg',
size: fileInfo.size,
width: 1432,
height: 1025,
},
IMAGE_SIZE_CONFIG_2K_1MB,
)
}
export async function openPicker(): Promise<PickerImage[]> {
return [await getFile()]
}
+4 -2
View File
@@ -1,11 +1,13 @@
import {type ImagePickerOptions} from 'expo-image-picker'
import {type OpenCropperOptions} from '@bsky.app/expo-image-crop-tool'
import {type PickerImage} from './picker.shared'
import {type CameraOpts} from './types'
export {openPicker, openUnifiedPicker} from './picker.shared'
export async function openCamera(_opts: CameraOpts): Promise<PickerImage> {
export async function openCamera(
_opts: ImagePickerOptions,
): Promise<PickerImage> {
throw new Error('openCamera is not supported on web')
}
-7
View File
@@ -8,10 +8,3 @@ export interface PickerOpts {
multiple?: boolean
maxFiles?: number
}
export interface CameraOpts {
width: number
height: number
freeStyleCropEnabled?: boolean
cropperCircleOverlay?: boolean
}
+1
View File
@@ -70,6 +70,7 @@ export type CommonNavigatorParams = {
ActivityPrivacySettings: undefined
ContentAndMediaSettings: undefined
NotificationSettings: undefined
ActivityNotificationSettings: undefined
InterestsSettings: undefined
AboutSettings: undefined
AppIconSettings: undefined
+149 -153
View File
@@ -98,12 +98,12 @@ msgid "{0, plural, one {# hour} other {# hours}}"
msgstr ""
#. placeholder {0}: labels.length
#: src/components/moderation/PostAlerts.tsx:149
#: src/components/moderation/PostAlerts.tsx:157
msgid "{0, plural, one {# label applied to your post} other {# labels applied to your post}}"
msgstr "{0, plural, one {# label applied to your post} other {# labels applied to your post}}"
#. placeholder {0}: labels.length
#: src/components/moderation/PostAlerts.tsx:156
#: src/components/moderation/PostAlerts.tsx:164
msgid "{0, plural, one {# label applied} other {# labels applied}}"
msgstr "{0, plural, one {# label applied} other {# labels applied}}"
@@ -219,7 +219,7 @@ msgid "{0} · {1}"
msgstr ""
#. placeholder {0}: desc.name
#: src/components/moderation/ContentHider.tsx:100
#: src/components/moderation/ContentHider.tsx:98
msgid "{0} (Account)"
msgstr ""
@@ -837,7 +837,7 @@ msgstr ""
#. Number of images beyond the first 3
#. placeholder {0}: labels.length
#. placeholder {0}: totalNumber - 3
#: src/components/moderation/PostAlerts.tsx:155
#: src/components/moderation/PostAlerts.tsx:163
#: src/view/com/composer/ComposerReplyTo.tsx:278
msgid "+{0}"
msgstr "+{0}"
@@ -966,7 +966,7 @@ msgstr ""
msgid "7 days"
msgstr ""
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:17
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:27
msgid "A collection of popular feeds you can find on Bluesky, including News, Booksky, Game Dev, Blacksky, and Fountain Pens"
msgstr ""
@@ -1333,7 +1333,7 @@ msgstr ""
msgid "Add to lists"
msgstr ""
#: src/components/PostControls/BookmarkButton.tsx:144
#: src/components/PostControls/BookmarkButton.tsx:121
msgid "Add to saved posts"
msgstr ""
@@ -1395,7 +1395,7 @@ msgstr ""
msgid "Adult content"
msgstr ""
#: src/components/moderation/ContentHider.tsx:131
#: src/components/moderation/ContentHider.tsx:129
#: src/lib/moderation/useGlobalLabelStrings.ts:34
#: src/lib/moderation/useModerationCauseDescription.ts:149
#: src/view/com/composer/labels/LabelsBtn.tsx:123
@@ -1555,7 +1555,7 @@ msgstr ""
msgid "Already have a code?"
msgstr ""
#: src/components/WelcomeModal.tsx:192
#: src/components/WelcomeModal.tsx:189
msgid "Already have an account?"
msgstr ""
@@ -1609,7 +1609,7 @@ msgstr ""
msgid "An error has occurred"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:448
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:434
msgid "An error occurred"
msgstr ""
@@ -1626,7 +1626,7 @@ msgstr ""
msgid "An error occurred while fetching the feed."
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:374
#: src/components/StarterPack/ProfileStarterPacks.tsx:377
msgid "An error occurred while generating your starter pack. Want to try again?"
msgstr ""
@@ -1679,7 +1679,7 @@ msgstr ""
msgid "An illustration depicting user avatars flowing from a contact book into the Bluesky app"
msgstr ""
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:35
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:45
msgid "An illustration of several Bluesky posts alongside repost, like, and comment icons"
msgstr ""
@@ -2114,8 +2114,8 @@ msgid "Before creating a post or replying, you must first verify your email."
msgstr ""
#: src/components/dialogs/StarterPackDialog.tsx:72
#: src/components/StarterPack/ProfileStarterPacks.tsx:264
#: src/components/StarterPack/ProfileStarterPacks.tsx:274
#: src/components/StarterPack/ProfileStarterPacks.tsx:267
#: src/components/StarterPack/ProfileStarterPacks.tsx:277
#: src/view/screens/Profile.tsx:350
msgid "Before creating a starter pack, you must first verify your email."
msgstr ""
@@ -2130,10 +2130,10 @@ msgstr ""
#: src/components/dms/dialogs/NewChatDialog.tsx:155
#: src/components/dms/MessageProfileButton.tsx:60
#: src/screens/Messages/ChatList.tsx:169
#: src/screens/Messages/ChatList.tsx:187
#: src/screens/Messages/ChatList.tsx:288
#: src/screens/Messages/ChatList.tsx:544
#: src/screens/Messages/ChatList.tsx:168
#: src/screens/Messages/ChatList.tsx:186
#: src/screens/Messages/ChatList.tsx:287
#: src/screens/Messages/ChatList.tsx:543
#: src/screens/Messages/Conversation.tsx:203
#: src/screens/Messages/ConversationSettings/MemberMenu.tsx:99
msgid "Before you can message another user, you must first verify your email."
@@ -2277,7 +2277,7 @@ msgstr ""
msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours."
msgstr ""
#: src/screens/Profile/Sections/Labels.tsx:203
#: src/screens/Profile/Sections/Labels.tsx:204
msgid "Blocking does not prevent this labeler from placing labels on your account."
msgstr ""
@@ -2353,7 +2353,7 @@ msgstr ""
msgid "Bluesky stores your contacts as encoded data. Removing your contacts will immediately delete this data."
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:340
#: src/components/StarterPack/ProfileStarterPacks.tsx:343
msgid "Bluesky will choose a set of recommended accounts from people in your network."
msgstr ""
@@ -2503,7 +2503,7 @@ msgstr ""
msgid "By you"
msgstr ""
#: src/view/com/composer/photos/OpenCameraBtn.tsx:69
#: src/view/com/composer/photos/OpenCameraBtn.tsx:65
msgid "Camera"
msgstr ""
@@ -2580,7 +2580,7 @@ msgstr ""
msgid "Cancel reply"
msgstr "Cancel reply"
#: src/screens/Search/Shell.tsx:595
#: src/screens/Search/Shell.tsx:584
msgid "Cancel search"
msgstr ""
@@ -2738,9 +2738,9 @@ msgstr ""
msgid "Chat not found."
msgstr "Chat not found."
#: src/screens/Messages/ChatList.tsx:570
#: src/screens/Messages/ChatList.tsx:605
#: src/screens/Messages/ChatList.tsx:652
#: src/screens/Messages/ChatList.tsx:569
#: src/screens/Messages/ChatList.tsx:604
#: src/screens/Messages/ChatList.tsx:651
msgid "Chat options"
msgstr "Chat options"
@@ -2763,10 +2763,10 @@ msgstr ""
#: src/components/dms/ConvoMenu.tsx:88
#: src/Navigation.tsx:515
#: src/screens/Messages/ChatList.tsx:98
#: src/screens/Messages/ChatList.tsx:102
#: src/screens/Messages/ChatList.tsx:672
#: src/screens/Messages/ChatList.tsx:682
#: src/screens/Messages/ChatList.tsx:97
#: src/screens/Messages/ChatList.tsx:101
#: src/screens/Messages/ChatList.tsx:671
#: src/screens/Messages/ChatList.tsx:681
#: src/screens/Messages/Settings.tsx:48
msgid "Chat settings"
msgstr ""
@@ -2793,9 +2793,9 @@ msgctxt "toast"
msgid "Chat unmuted"
msgstr ""
#: src/screens/Messages/ChatList.tsx:92
#: src/screens/Messages/ChatList.tsx:556
#: src/screens/Messages/ChatList.tsx:596
#: src/screens/Messages/ChatList.tsx:91
#: src/screens/Messages/ChatList.tsx:555
#: src/screens/Messages/ChatList.tsx:595
msgid "Chats"
msgstr ""
@@ -2832,7 +2832,7 @@ msgstr ""
msgid "Choose Feeds"
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:349
#: src/components/StarterPack/ProfileStarterPacks.tsx:352
msgid "Choose for me"
msgstr ""
@@ -3022,8 +3022,8 @@ msgstr ""
msgid "Close"
msgstr ""
#: src/components/Dialog/index.web.tsx:127
#: src/components/Dialog/index.web.tsx:317
#: src/components/Dialog/index.web.tsx:129
#: src/components/Dialog/index.web.tsx:339
msgid "Close active dialog"
msgstr ""
@@ -3085,7 +3085,7 @@ msgstr "Close the incoming requests banner"
msgid "Close this dialog"
msgstr ""
#: src/components/WelcomeModal.tsx:217
#: src/components/WelcomeModal.tsx:214
msgid "Close welcome modal"
msgstr ""
@@ -3278,7 +3278,7 @@ msgid "Content promoting or depicting self-harm"
msgstr ""
#: src/components/moderation/ModerationDetailsDialog.tsx:70
#: src/components/moderation/ScreenHider.tsx:100
#: src/components/moderation/ScreenHider.tsx:98
#: src/lib/moderation/useGlobalLabelStrings.ts:22
#: src/lib/moderation/useModerationCauseDescription.ts:46
msgid "Content Warning"
@@ -3288,7 +3288,7 @@ msgstr ""
msgid "Content warnings"
msgstr ""
#: src/components/Menu/index.web.tsx:91
#: src/components/Menu/index.web.tsx:93
msgid "Context menu backdrop, click to close the menu."
msgstr ""
@@ -3363,7 +3363,7 @@ msgstr "Conversation not found."
msgid "Copied build version to clipboard"
msgstr ""
#: src/screens/Search/Shell.tsx:515
#: src/screens/Search/Shell.tsx:504
msgid "Copied link to clipboard"
msgstr "Copied link to clipboard"
@@ -3602,7 +3602,7 @@ msgstr "cows pigs"
#: src/components/dialogs/StarterPackDialog.tsx:113
#: src/components/dialogs/StarterPackDialog.tsx:210
#: src/components/dms/InitiateChatFlow.tsx:502
#: src/components/StarterPack/ProfileStarterPacks.tsx:329
#: src/components/StarterPack/ProfileStarterPacks.tsx:332
msgid "Create"
msgstr ""
@@ -3619,8 +3619,8 @@ msgstr ""
msgid "Create a QR code for a starter pack"
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:207
#: src/components/StarterPack/ProfileStarterPacks.tsx:316
#: src/components/StarterPack/ProfileStarterPacks.tsx:210
#: src/components/StarterPack/ProfileStarterPacks.tsx:319
#: src/Navigation.tsx:551
msgid "Create a starter pack"
msgstr ""
@@ -3630,12 +3630,12 @@ msgstr ""
msgid "Create a Starter Pack"
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:303
#: src/components/StarterPack/ProfileStarterPacks.tsx:306
msgid "Create a starter pack for me"
msgstr ""
#: src/components/WelcomeModal.tsx:158
#: src/components/WelcomeModal.tsx:166
#: src/components/WelcomeModal.tsx:155
#: src/components/WelcomeModal.tsx:163
#: src/screens/Messages/JoinRequest.tsx:310
#: src/screens/Signup/index.tsx:141
#: src/view/com/auth/SplashScreen.tsx:106
@@ -3665,7 +3665,7 @@ msgstr ""
msgid "Create an avatar instead"
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:214
#: src/components/StarterPack/ProfileStarterPacks.tsx:217
msgid "Create another"
msgstr ""
@@ -3997,7 +3997,7 @@ msgstr "Disable"
msgid "Disable 2FA"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:414
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:400
msgid "Disable captions"
msgstr "Disable captions"
@@ -4089,7 +4089,7 @@ msgstr ""
msgid "Discover New Feeds"
msgstr ""
#: src/components/Dialog/index.tsx:413
#: src/components/Dialog/index.tsx:414
#: src/features/inviteFriends/components/FollowersPromoBanner.tsx:83
msgid "Dismiss"
msgstr ""
@@ -4137,7 +4137,7 @@ msgstr ""
msgid "Display name"
msgstr ""
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:23
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:33
msgid "Ditch the trolls and clickbait. Find real people and conversations that matter to you."
msgstr ""
@@ -4215,7 +4215,7 @@ msgstr ""
msgid "Double tap or long press the message to add a reaction"
msgstr ""
#: src/components/Dialog/index.tsx:414
#: src/components/Dialog/index.tsx:415
msgid "Double tap to close the dialog"
msgstr ""
@@ -4504,7 +4504,7 @@ msgstr ""
msgid "Embed this post in your website. Simply copy the following snippet and paste it into the HTML code of your website."
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx:61
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx:64
msgid "Embedded video player"
msgstr ""
@@ -4528,7 +4528,7 @@ msgstr ""
msgid "Enable beta features"
msgstr "Enable beta features"
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:415
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:401
msgid "Enable captions"
msgstr "Enable captions"
@@ -4576,7 +4576,7 @@ msgstr ""
msgid "Enabled"
msgstr ""
#: src/screens/Profile/Sections/Feed.tsx:131
#: src/screens/Profile/Sections/Feed.tsx:132
msgid "End of feed"
msgstr ""
@@ -4603,7 +4603,7 @@ msgstr ""
msgid "Enter code"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:433
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:419
msgid "Enter fullscreen"
msgstr ""
@@ -4733,7 +4733,7 @@ msgstr ""
msgid "Excludes users you follow"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:432
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:418
msgid "Exit fullscreen"
msgstr ""
@@ -4750,7 +4750,7 @@ msgstr ""
msgid "Expand or collapse the full post you are replying to"
msgstr ""
#: src/components/Post/ShowMoreTextButton.tsx:33
#: src/components/Post/ShowMoreTextButton.tsx:31
msgid "Expand post text"
msgstr ""
@@ -4798,14 +4798,14 @@ msgid "Explicit sexual images."
msgstr ""
#: src/Navigation.tsx:760
#: src/screens/Search/Shell.tsx:552
#: src/screens/Search/Shell.tsx:541
#: src/view/shell/desktop/LeftNav.tsx:677
#: src/view/shell/Drawer.tsx:473
msgid "Explore"
msgstr ""
#: src/components/WelcomeModal.tsx:171
#: src/components/WelcomeModal.tsx:180
#: src/components/WelcomeModal.tsx:168
#: src/components/WelcomeModal.tsx:177
msgid "Explore the app"
msgstr ""
@@ -4881,7 +4881,7 @@ msgstr ""
#: src/components/Lightbox/Lightbox.web.tsx:302
#: src/features/inviteFriends/InviteFriendsDialogInner.tsx:130
#: src/screens/Search/Shell.tsx:516
#: src/screens/Search/Shell.tsx:505
msgid "Failed to copy link"
msgstr "Failed to copy link"
@@ -4969,7 +4969,7 @@ msgctxt "toast"
msgid "Failed to leave group chat"
msgstr "Failed to leave group chat"
#: src/screens/Messages/ChatList.tsx:421
#: src/screens/Messages/ChatList.tsx:420
#: src/screens/Messages/Inbox.tsx:202
msgid "Failed to load conversations"
msgstr ""
@@ -5022,12 +5022,12 @@ msgstr ""
msgid "Failed to lock group chat"
msgstr "Failed to lock group chat"
#: src/screens/Messages/ChatList.tsx:637
#: src/screens/Messages/ChatList.tsx:636
#: src/view/shell/bottom-bar/BottomBar.tsx:447
msgid "Failed to mark all chats as read"
msgstr "Failed to mark all chats as read"
#: src/screens/Messages/ChatList.tsx:646
#: src/screens/Messages/ChatList.tsx:645
#: src/screens/Messages/Inbox.tsx:304
#: src/view/shell/bottom-bar/BottomBar.tsx:456
msgid "Failed to mark all requests as read"
@@ -5369,7 +5369,7 @@ msgstr ""
msgid "Find people you know"
msgstr "Find people you know"
#: src/screens/Search/Shell.tsx:765
#: src/screens/Search/Shell.tsx:753
msgid "Find posts, users, and feeds on Bluesky"
msgstr ""
@@ -5382,7 +5382,7 @@ msgstr ""
msgid "Find your friends on Bluesky by verifying your phone number and matching with your contacts. We protect your information and you control what happens next. <0>Learn more</0>"
msgstr ""
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:21
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:31
msgid "Find your people"
msgstr ""
@@ -5623,7 +5623,7 @@ msgstr ""
msgid "Forever"
msgstr ""
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:30
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:40
msgid "Forget the noise"
msgstr ""
@@ -5642,7 +5642,7 @@ msgstr ""
msgid "Four message bubbles representing a group chat. First message: \"Did you hear the news? Bluesky has group chats now!\" Second message: \"omg, no way\" Third message: \"Wow, 50 people in one chat!\" Fourth message: \"You can send invite links too!\""
msgstr "Four message bubbles representing a group chat. First message: \"Did you hear the news? Bluesky has group chats now!\" Second message: \"omg, no way\" Third message: \"Wow, 50 people in one chat!\" Fourth message: \"You can send invite links too!\""
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:12
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:22
msgid "Free your feed"
msgstr ""
@@ -5669,7 +5669,7 @@ msgstr ""
msgid "From these people"
msgstr "From these people"
#: src/components/StarterPack/ProfileStarterPacks.tsx:337
#: src/components/StarterPack/ProfileStarterPacks.tsx:340
msgid "Generate a starter pack"
msgstr ""
@@ -5808,9 +5808,9 @@ msgstr ""
#: src/components/dialogs/LinkWarning.tsx:127
#: src/components/dialogs/LinkWarning.tsx:133
#: src/components/Layout/Header/index.tsx:133
#: src/components/moderation/ScreenHider.tsx:161
#: src/components/moderation/ScreenHider.tsx:170
#: src/components/Layout/Header/index.tsx:134
#: src/components/moderation/ScreenHider.tsx:157
#: src/components/moderation/ScreenHider.tsx:166
#: src/screens/Login/components/AuthLayout/Header/index.tsx:74
#: src/screens/Login/components/ConfirmHostingProviderDialog.tsx:160
#: src/screens/Login/components/ConfirmHostingProviderDialog.tsx:163
@@ -6145,7 +6145,7 @@ msgstr ""
#: src/components/interstitials/Trending.tsx:133
#: src/components/interstitials/TrendingVideos.tsx:139
#: src/components/moderation/ContentHider.tsx:220
#: src/components/moderation/ContentHider.tsx:217
#: src/components/moderation/LabelPreference.tsx:141
#: src/components/moderation/PostHider.tsx:140
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:820
@@ -6235,7 +6235,7 @@ msgstr ""
msgid "Hide verification badges"
msgstr ""
#: src/components/moderation/ContentHider.tsx:171
#: src/components/moderation/ContentHider.tsx:168
#: src/components/moderation/PostHider.tsx:94
msgid "Hides the content"
msgstr ""
@@ -6543,7 +6543,7 @@ msgstr "In-app, push, everyone"
msgid "In-app, push, people you follow"
msgstr "In-app, push, people you follow"
#: src/screens/Messages/ChatList.tsx:438
#: src/screens/Messages/ChatList.tsx:437
msgid "Inbox empty"
msgstr "Inbox empty"
@@ -6826,11 +6826,11 @@ msgid "KWS website"
msgstr ""
#. placeholder {0}: sanitizeDisplayName(desc.source!)
#: src/components/moderation/ContentHider.tsx:251
#: src/components/moderation/ContentHider.tsx:245
msgid "Labeled by {0}."
msgstr ""
#: src/components/moderation/ContentHider.tsx:249
#: src/components/moderation/ContentHider.tsx:243
msgid "Labeled by the author."
msgstr ""
@@ -6847,7 +6847,7 @@ msgstr ""
msgid "Labels applied to this post"
msgstr "Labels applied to this post"
#: src/screens/Profile/Sections/Labels.tsx:194
#: src/screens/Profile/Sections/Labels.tsx:195
msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network."
msgstr ""
@@ -6899,7 +6899,7 @@ msgctxt "english-only-resource"
msgid "Learn more"
msgstr ""
#: src/components/moderation/ScreenHider.tsx:147
#: src/components/moderation/ScreenHider.tsx:143
msgid "Learn More"
msgstr ""
@@ -6932,8 +6932,8 @@ msgstr ""
msgid "Learn more about self hosting your PDS."
msgstr ""
#: src/components/moderation/ContentHider.tsx:169
#: src/components/moderation/ContentHider.tsx:235
#: src/components/moderation/ContentHider.tsx:166
#: src/components/moderation/ContentHider.tsx:230
msgid "Learn more about the moderation applied to this content"
msgstr ""
@@ -6946,7 +6946,7 @@ msgid "Learn more about these changes and how to share your thoughts with us by
msgstr ""
#: src/components/moderation/PostHider.tsx:116
#: src/components/moderation/ScreenHider.tsx:134
#: src/components/moderation/ScreenHider.tsx:132
msgid "Learn more about this warning"
msgstr ""
@@ -6970,7 +6970,7 @@ msgid "Learn more in your <0>account settings.</0>"
msgstr ""
#: src/components/dialogs/ServerInput.tsx:222
#: src/components/moderation/ContentHider.tsx:259
#: src/components/moderation/ContentHider.tsx:253
#: src/screens/Login/components/HostingProviderDialog.tsx:243
msgid "Learn more."
msgstr ""
@@ -7037,7 +7037,7 @@ msgstr "Left group chat."
msgid "left to go."
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:354
#: src/components/StarterPack/ProfileStarterPacks.tsx:357
msgid "Let me choose"
msgstr ""
@@ -7288,7 +7288,7 @@ msgid "Load new notifications"
msgstr ""
#: src/screens/Profile/ProfileFeed/index.tsx:206
#: src/screens/Profile/Sections/Feed.tsx:117
#: src/screens/Profile/Sections/Feed.tsx:118
#: src/screens/ProfileList/FeedSection.tsx:113
#: src/view/com/feeds/FeedPage.tsx:168
msgid "Load new posts"
@@ -7382,7 +7382,7 @@ msgstr "Love GIFs"
msgid "Make adjustments to email settings for your account"
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:311
#: src/components/StarterPack/ProfileStarterPacks.tsx:314
msgid "Make one for me"
msgstr ""
@@ -7413,15 +7413,15 @@ msgstr "Manual"
msgid "Mark all as read"
msgstr ""
#: src/screens/Messages/ChatList.tsx:656
#: src/screens/Messages/ChatList.tsx:660
#: src/screens/Messages/ChatList.tsx:655
#: src/screens/Messages/ChatList.tsx:659
#: src/view/shell/bottom-bar/BottomBar.tsx:465
#: src/view/shell/bottom-bar/BottomBar.tsx:469
msgid "Mark all chats as read"
msgstr "Mark all chats as read"
#: src/screens/Messages/ChatList.tsx:664
#: src/screens/Messages/ChatList.tsx:668
#: src/screens/Messages/ChatList.tsx:663
#: src/screens/Messages/ChatList.tsx:667
#: src/view/shell/bottom-bar/BottomBar.tsx:473
#: src/view/shell/bottom-bar/BottomBar.tsx:477
msgid "Mark all requests as read"
@@ -7436,12 +7436,12 @@ msgstr ""
msgid "Marked all as read"
msgstr ""
#: src/screens/Messages/ChatList.tsx:634
#: src/screens/Messages/ChatList.tsx:633
#: src/view/shell/bottom-bar/BottomBar.tsx:444
msgid "Marked all chats as read"
msgstr "Marked all chats as read"
#: src/screens/Messages/ChatList.tsx:643
#: src/screens/Messages/ChatList.tsx:642
#: src/view/shell/bottom-bar/BottomBar.tsx:453
msgid "Marked all requests as read"
msgstr "Marked all requests as read"
@@ -7888,11 +7888,11 @@ msgstr ""
#: src/components/dms/dialogs/NewChatDialog.tsx:169
#: src/components/dms/dialogs/NewChatDialog.tsx:184
#: src/screens/Messages/ChatList.tsx:231
#: src/screens/Messages/ChatList.tsx:232
#: src/screens/Messages/ChatList.tsx:233
#: src/screens/Messages/ChatList.tsx:455
#: src/screens/Messages/ChatList.tsx:456
#: src/screens/Messages/ChatList.tsx:457
#: src/screens/Messages/ChatList.tsx:581
#: src/screens/Messages/ChatList.tsx:580
msgid "New chat"
msgstr ""
@@ -8052,7 +8052,7 @@ msgstr ""
msgid "Night"
msgstr "Night"
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:32
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:42
msgid "No ads, no invasive tracking, no engagement traps. Bluesky respects your time and attention."
msgstr ""
@@ -8138,7 +8138,7 @@ msgstr ""
msgid "No messages yet"
msgstr ""
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:14
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:24
msgid "No more doomscrolling junk-filled algorithms. Find feeds that work for you, not against you."
msgstr ""
@@ -8179,12 +8179,12 @@ msgstr "No one can send messages"
msgid "No posts here"
msgstr ""
#: src/screens/Profile/Sections/Feed.tsx:81
#: src/screens/Profile/Sections/Feed.tsx:82
#: src/view/screens/Profile.tsx:430
msgid "No posts yet"
msgstr ""
#: src/view/com/post-thread/PostQuotes.tsx:107
#: src/view/com/post-thread/PostQuotes.tsx:114
msgid "No quotes yet"
msgstr ""
@@ -8273,7 +8273,7 @@ msgstr ""
msgid "Nobody has liked this yet. Maybe you should be the first!"
msgstr ""
#: src/view/com/post-thread/PostQuotes.tsx:109
#: src/view/com/post-thread/PostQuotes.tsx:116
msgid "Nobody has quoted this yet. Maybe you should be the first!"
msgstr ""
@@ -8514,7 +8514,7 @@ msgstr "Only posts with videos"
msgid "Only replies"
msgstr "Only replies"
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:41
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:39
msgid "Only WebVTT (.vtt) files are supported"
msgstr ""
@@ -8527,8 +8527,8 @@ msgid "Oops, this profile doesn't exist. Try scanning another one."
msgstr "Oops, this profile doesn't exist. Try scanning another one."
#: src/components/Lists.tsx:184
#: src/components/StarterPack/ProfileStarterPacks.tsx:363
#: src/components/StarterPack/ProfileStarterPacks.tsx:372
#: src/components/StarterPack/ProfileStarterPacks.tsx:366
#: src/components/StarterPack/ProfileStarterPacks.tsx:375
#: src/screens/Settings/AppPasswords.tsx:59
#: src/screens/Settings/components/ChangeHandleDialog.tsx:108
#: src/view/screens/Profile.tsx:132
@@ -8565,7 +8565,7 @@ msgstr ""
msgid "Open draft"
msgstr ""
#: src/components/Layout/Header/index.tsx:167
#: src/components/Layout/Header/index.tsx:168
msgid "Open drawer menu"
msgstr ""
@@ -8694,7 +8694,7 @@ msgstr ""
msgid "Opens alt text dialog"
msgstr ""
#: src/view/com/composer/photos/OpenCameraBtn.tsx:70
#: src/view/com/composer/photos/OpenCameraBtn.tsx:66
msgid "Opens camera on device"
msgstr ""
@@ -8912,7 +8912,7 @@ msgid "Password updated!"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:395
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:381
msgid "Pause"
msgstr ""
@@ -8920,7 +8920,7 @@ msgstr ""
msgid "Pause GIF"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:346
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:332
msgid "Pause video"
msgstr ""
@@ -9058,7 +9058,7 @@ msgid "Pinned to your feeds"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:396
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:382
msgid "Play"
msgstr ""
@@ -9072,7 +9072,7 @@ msgid "Play GIF"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/index.tsx:169
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:347
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:333
msgid "Play video"
msgstr ""
@@ -9364,10 +9364,6 @@ msgctxt "toast"
msgid "Post pinned"
msgstr ""
#: src/components/PostControls/BookmarkButton.tsx:67
msgid "Post saved"
msgstr ""
#: src/state/queries/pinned-post.ts:61
msgctxt "toast"
msgid "Post unpinned"
@@ -9682,11 +9678,11 @@ msgstr ""
msgid "Read the Bluesky Terms of Service"
msgstr ""
#: src/components/WelcomeModal.tsx:149
#: src/components/WelcomeModal.tsx:146
msgid "Real conversations."
msgstr ""
#: src/components/WelcomeModal.tsx:147
#: src/components/WelcomeModal.tsx:144
msgid "Real people."
msgstr ""
@@ -9743,7 +9739,7 @@ msgstr ""
msgid "Reject join request"
msgstr "Reject join request"
#: src/screens/Messages/ChatList.tsx:425
#: src/screens/Messages/ChatList.tsx:424
#: src/screens/Messages/Inbox.tsx:206
msgid "Reload conversations"
msgstr ""
@@ -9857,7 +9853,7 @@ msgstr ""
msgid "Remove from saved feeds"
msgstr ""
#: src/components/PostControls/BookmarkButton.tsx:143
#: src/components/PostControls/BookmarkButton.tsx:120
#: src/screens/Bookmarks.tsx:252
msgid "Remove from saved posts"
msgstr ""
@@ -9938,7 +9934,7 @@ msgstr ""
msgid "Removed from saved feeds"
msgstr ""
#: src/components/PostControls/BookmarkButton.tsx:107
#: src/components/PostControls/BookmarkButton.tsx:88
#: src/screens/Bookmarks.tsx:211
msgid "Removed from saved posts"
msgstr ""
@@ -10353,10 +10349,10 @@ msgstr ""
#: src/components/moderation/ReportDialog/index.tsx:297
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:56
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:59
#: src/components/StarterPack/ProfileStarterPacks.tsx:377
#: src/components/StarterPack/ProfileStarterPacks.tsx:380
#: src/screens/Login/LoginForm.tsx:531
#: src/screens/Login/LoginForm.tsx:537
#: src/screens/Messages/ChatList.tsx:430
#: src/screens/Messages/ChatList.tsx:429
#: src/screens/Messages/components/MessageListError.tsx:24
#: src/screens/Messages/Inbox.tsx:211
#: src/screens/Messages/JoinRequests.tsx:361
@@ -10515,8 +10511,8 @@ msgstr ""
msgid "Say hello!"
msgstr ""
#: src/screens/Messages/ChatList.tsx:223
#: src/screens/Messages/ChatList.tsx:447
#: src/screens/Messages/ChatList.tsx:222
#: src/screens/Messages/ChatList.tsx:446
msgid "Say hi to someone"
msgstr "Say hi to someone"
@@ -10559,9 +10555,9 @@ msgstr ""
#: src/components/forms/SearchInput.tsx:53
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:218
#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:224
#: src/screens/Search/Shell.tsx:552
#: src/screens/Search/Shell.tsx:616
#: src/screens/Search/Shell.tsx:753
#: src/screens/Search/Shell.tsx:541
#: src/screens/Search/Shell.tsx:604
#: src/screens/Search/Shell.tsx:741
#: src/view/shell/bottom-bar/BottomBar.tsx:216
msgid "Search"
msgstr ""
@@ -10775,8 +10771,8 @@ msgstr ""
msgid "Select app language"
msgstr ""
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:60
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:67
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:58
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:65
msgid "Select caption file (.vtt)"
msgstr "Select caption file (.vtt)"
@@ -11168,8 +11164,8 @@ msgstr ""
msgid "Share this feed"
msgstr ""
#: src/screens/Search/Shell.tsx:568
#: src/screens/Search/Shell.tsx:638
#: src/screens/Search/Shell.tsx:557
#: src/screens/Search/Shell.tsx:626
msgid "Share this search"
msgstr "Share this search"
@@ -11214,7 +11210,7 @@ msgstr "Sharing your age range reveals only the range associated with your Apple
msgid "Sharing your age range reveals only the range associated with your Google Account – for example, that you’re at least 18. <0>Your exact age and birthday are never shared,</0> and this data never leaves this device. Therefore, it only enables access on this device. Alternatively, <1>you can use our trusted partner, KWS</1>, to complete your verification and enable access on all platforms."
msgstr "Sharing your age range reveals only the range associated with your Google Account – for example, that you’re at least 18. <0>Your exact age and birthday are never shared,</0> and this data never leaves this device. Therefore, it only enables access on this device. Alternatively, <1>you can use our trusted partner, KWS</1>, to complete your verification and enable access on all platforms."
#: src/components/moderation/ContentHider.tsx:220
#: src/components/moderation/ContentHider.tsx:217
#: src/components/moderation/LabelPreference.tsx:143
#: src/components/moderation/PostHider.tsx:140
msgid "Show"
@@ -11224,8 +11220,8 @@ msgstr ""
msgid "Show alt text"
msgstr ""
#: src/components/moderation/ScreenHider.tsx:179
#: src/components/moderation/ScreenHider.tsx:182
#: src/components/moderation/ScreenHider.tsx:175
#: src/components/moderation/ScreenHider.tsx:178
#: src/features/liveNow/components/LiveStatusDialog.tsx:318
#: src/features/liveNow/components/LiveStatusDialog.tsx:322
#: src/screens/List/ListHiddenScreen.tsx:194
@@ -11269,9 +11265,9 @@ msgstr ""
msgid "Show lists of users to select from"
msgstr ""
#: src/components/Post/ShowMoreTextButton.tsx:52
msgid "Show More"
msgstr ""
#: src/components/Post/ShowMoreTextButton.tsx:50
msgid "Show more"
msgstr "Show more"
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:594
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:596
@@ -11336,15 +11332,15 @@ msgstr ""
msgid "Shows other accounts you can switch to"
msgstr ""
#: src/components/moderation/ContentHider.tsx:172
#: src/components/moderation/ContentHider.tsx:169
#: src/components/moderation/PostHider.tsx:94
msgid "Shows the content"
msgstr ""
#: src/components/dialogs/Signin.tsx:98
#: src/components/dialogs/Signin.tsx:100
#: src/components/WelcomeModal.tsx:197
#: src/components/WelcomeModal.tsx:209
#: src/components/WelcomeModal.tsx:194
#: src/components/WelcomeModal.tsx:206
#: src/screens/Hashtag.tsx:231
#: src/screens/Login/index.tsx:143
#: src/screens/Login/index.tsx:165
@@ -11431,7 +11427,7 @@ msgstr ""
msgid "Sign up"
msgstr "Sign up"
#: src/components/moderation/ScreenHider.tsx:98
#: src/components/moderation/ScreenHider.tsx:96
#: src/lib/moderation/useGlobalLabelStrings.ts:28
msgid "Sign-in Required"
msgstr ""
@@ -11494,7 +11490,7 @@ msgstr ""
msgid "So many thoughts, you should post one"
msgstr ""
#: src/components/WelcomeModal.tsx:151
#: src/components/WelcomeModal.tsx:148
msgid "Social media you control."
msgstr ""
@@ -11588,7 +11584,7 @@ msgid "Something went wrong, please try again"
msgstr ""
#: src/screens/Moderation/index.tsx:112
#: src/screens/Profile/Sections/Labels.tsx:184
#: src/screens/Profile/Sections/Labels.tsx:185
#: src/screens/Settings/BetaFeaturesSettings.tsx:80
#: src/screens/Settings/components/BetaFeaturesFeedbackDialog.tsx:84
msgid "Something went wrong, please try again."
@@ -11715,7 +11711,7 @@ msgstr ""
msgid "Starter Packs"
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:295
#: src/components/StarterPack/ProfileStarterPacks.tsx:298
msgid "Starter packs let you easily share your favorite feeds and people with your friends."
msgstr ""
@@ -11806,7 +11802,7 @@ msgid "Subscribe to {publicationTitle} on {0}"
msgstr "Subscribe to {publicationTitle} on {0}"
#. placeholder {0}: labelerInfo.creator.handle
#: src/screens/Profile/Sections/Labels.tsx:231
#: src/screens/Profile/Sections/Labels.tsx:232
msgid "Subscribe to @{0} to use these labels:"
msgstr ""
@@ -12108,7 +12104,7 @@ msgstr ""
msgid "That username is already taken"
msgstr ""
#: src/view/com/post-thread/PostQuotes.tsx:135
#: src/view/com/post-thread/PostQuotes.tsx:142
msgid "That's all, folks!"
msgstr ""
@@ -12387,7 +12383,7 @@ msgstr "They own this chat"
msgid "They won’t be able to rejoin unless you invite them again."
msgstr "They won’t be able to rejoin unless you invite them again."
#: src/components/moderation/ScreenHider.tsx:118
#: src/components/moderation/ScreenHider.tsx:116
msgid "This {screenDescription} has been flagged:"
msgstr ""
@@ -12403,7 +12399,7 @@ msgstr "This account has been marked as automated by its owner."
msgid "This account has one or more attempted verifications, but it is not currently verified."
msgstr ""
#: src/components/moderation/ScreenHider.tsx:113
#: src/components/moderation/ScreenHider.tsx:111
msgid "This account has requested that users sign in to view their profile."
msgstr ""
@@ -12591,7 +12587,7 @@ msgstr ""
msgid "This label was applied to the entire user account and will appear on all posts."
msgstr "This label was applied to the entire user account and will appear on all posts."
#: src/screens/Profile/Sections/Labels.tsx:218
#: src/screens/Profile/Sections/Labels.tsx:219
msgid "This labeler hasn't declared what labels it publishes, and may not be active."
msgstr ""
@@ -13187,7 +13183,7 @@ msgstr "Unmute this group chat"
msgid "Unmute thread"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:344
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:330
msgid "Unmute video"
msgstr ""
@@ -13253,7 +13249,7 @@ msgstr ""
msgid "Unsubscribed from list"
msgstr ""
#: src/view/com/composer/text-input/TextInput.tsx:128
#: src/view/com/composer/text-input/TextInput.tsx:127
msgid "Unsupported clipboard content"
msgstr "Unsupported clipboard content"
@@ -13725,7 +13721,7 @@ msgid "View details"
msgstr ""
#: src/view/com/posts/ViewFullThread.tsx:31
#: src/view/com/posts/ViewFullThread.tsx:64
#: src/view/com/posts/ViewFullThread.tsx:65
msgid "View full thread"
msgstr ""
@@ -14156,7 +14152,7 @@ msgid "Who can verify?"
msgstr ""
#: src/screens/Home/NoFeedsPinned.tsx:80
#: src/screens/Messages/ChatList.tsx:410
#: src/screens/Messages/ChatList.tsx:409
#: src/screens/Messages/Inbox.tsx:191
msgid "Whoops!"
msgstr ""
@@ -14558,7 +14554,7 @@ msgstr ""
msgid "You have unsaved changes. Would you like to save them before viewing your drafts?"
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:292
#: src/components/StarterPack/ProfileStarterPacks.tsx:295
msgid "You haven't created a starter pack yet!"
msgstr ""
@@ -14617,7 +14613,7 @@ msgstr "You must be a chat owner to remove a member."
msgid "You must be at least 13 years old to use Bluesky. Read our <0>Terms of Service</0> for more information."
msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:365
#: src/components/StarterPack/ProfileStarterPacks.tsx:368
msgid "You must be following at least seven other people to generate a starter pack."
msgstr ""
@@ -14975,11 +14971,11 @@ msgid "Your preferred language"
msgstr ""
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx:102
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx:53
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx:54
msgid "Your profile picture"
msgstr ""
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:26
#: src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx:36
msgid "Your profile picture surrounded by concentric circles of other users' profile pictures"
msgstr ""
+13
View File
@@ -0,0 +1,13 @@
/*
* Used ONLY by the Android typecheck pass (tsconfig.check.android.json) - it
* is excluded from the other passes and has no runtime effect.
*
* react-native-device-attest's Android variant implements only the Play
* Integrity statics, so the iOS-only getDeviceCheckToken (whose app call
* site is IS_IOS-guarded) fails to resolve under .android module suffixes.
* Pin the module to its platform-neutral base class, which declares the
* full API surface.
*/
declare module 'react-native-device-attest' {
export {DeviceAttestBase as default} from 'react-native-device-attest/build/DeviceAttestBase'
}
+102
View File
@@ -0,0 +1,102 @@
/*
* Used ONLY by the web typecheck pass (tsconfig.check.web.json) - it is
* excluded from the other passes and has no runtime effect.
*
* Some packages publish .web.d.ts declarations that diverge from the API
* the app is written against (or, worse, raw TypeScript sources that break
* under `moduleSuffixes: [".web", ""]`), so this pins them to their native
* declarations. react-native-svg has the same problem, handled by deleting
* its divergent web declarations in patches/react-native-svg@15.12.1.patch
* so resolution falls through to the native ones.
*/
/*
* Side-effect CSS imports in .web files are handled by the bundler.
*/
declare module '*.css'
/*
* expo-file-system's declarations build File/Directory on top of
* `./ExpoFileSystem`, which remaps to a web shim whose classes are empty.
* The fully-typed base classes live in ExpoFileSystem.types (no .web
* sibling), so mirror the FileSystem.d.ts wrapper classes on top of those.
*/
declare module 'expo-file-system' {
import {
Directory as ExpoFileSystemDirectory,
File as ExpoFileSystemFile,
} from 'expo-file-system/build/ExpoFileSystem.types'
export {
type DirectoryCreateOptions,
type DirectoryInfo,
type DownloadOptions,
EncodingType,
type FileCreateOptions,
FileHandle,
type FileInfo,
type FileWriteOptions,
type InfoOptions,
type PathInfo,
} from 'expo-file-system/build/ExpoFileSystem.types'
import {type PathInfo as ExpoPathInfo} from 'expo-file-system/build/ExpoFileSystem.types'
import {PathUtilities} from 'expo-file-system/build/pathUtilities'
export class Paths extends PathUtilities {
static get cache(): Directory
static get bundle(): Directory
static get document(): Directory
static get appleSharedContainers(): Record<string, Directory>
static get totalDiskSpace(): number
static get availableDiskSpace(): number
static info(...uris: string[]): ExpoPathInfo
}
export class File extends ExpoFileSystemFile implements Blob {
constructor(...uris: (string | File | Directory)[])
get parentDirectory(): Directory
get extension(): string
get name(): string
readableStream(): ReadableStream<Uint8Array<ArrayBuffer>>
writableStream(): WritableStream<Uint8Array<ArrayBufferLike>>
arrayBuffer(): Promise<ArrayBuffer>
stream(): ReadableStream<Uint8Array<ArrayBuffer>>
slice(start?: number, end?: number, contentType?: string): Blob
}
export class Directory extends ExpoFileSystemDirectory {
constructor(...uris: (string | File | Directory)[])
get parentDirectory(): Directory
list(): (Directory | File)[]
get name(): string
createFile(name: string, mimeType: string | null): File
createDirectory(name: string): Directory
}
}
/*
* expo-image-manipulator's web declarations export the module class where
* the native ones export an instance, hiding instance methods such as
* `manipulate` from `typeof ImageManipulator`. Mirror the native surface.
*/
declare module 'expo-image-manipulator' {
import {type ImageManipulator as ImageManipulatorModule} from 'expo-image-manipulator/build/ImageManipulator.types'
export const ImageManipulator: ImageManipulatorModule
export {
manipulateAsync,
useImageManipulator,
} from 'expo-image-manipulator/build/ImageManipulator'
export {
type Action,
type ActionCrop,
type ActionExtent,
type ActionFlip,
type ActionResize,
type ActionRotate,
FlipType,
type ImageResult,
SaveFormat,
type SaveOptions,
} from 'expo-image-manipulator/build/ImageManipulator.types'
export {type ImageManipulatorContext} from 'expo-image-manipulator/build/ImageManipulatorContext'
export {type ImageRef} from 'expo-image-manipulator/build/ImageRef'
}
+1
View File
@@ -61,6 +61,7 @@ export const router = new Router<AllNavigatableRoutes>({
AboutSettings: '/settings/about',
AppIconSettings: '/settings/app-icon',
NotificationSettings: '/settings/notifications',
ActivityNotificationSettings: '/settings/notifications/activity',
FindContactsSettings: '/settings/find-contacts',
// support
Support: '/support',
+1 -2
View File
@@ -1,6 +1,5 @@
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {View} from 'react-native'
import {useAnimatedRef} from 'react-native-reanimated'
import {type ChatBskyActorGetStatus, type ChatBskyConvoDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {
@@ -275,7 +274,7 @@ export function ChatList({
const t = useTheme()
const {t: l} = useLingui()
const aa = useAgeAssurance()
const scrollElRef: ListRef = useAnimatedRef()
const scrollElRef: ListRef = useRef(null)
const {isWithinSplitView} = useIsWithinSplitView()
const openChatControl = useCallback(() => {
@@ -17,7 +17,6 @@ import Animated, {
runOnJS,
type ScrollEvent,
type SharedValue,
useAnimatedRef,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
@@ -154,7 +153,7 @@ export function MessagesList({
const t = useTheme()
const textInputId = 'chat-input-' + useId()
const flatListRef = useAnimatedRef<ListMethods>()
const flatListRef = useRef<ListMethods | null>(null)
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
() => new Set(),
@@ -1,3 +1,8 @@
export function StepFindContacts() {
import {type Action, type State} from '#/components/contacts/state'
export function StepFindContacts(_props: {
flowState: State
flowDispatch: React.ActionDispatch<[Action]>
}): never {
throw new Error('StepFindContacts is not available on web')
}
@@ -1,3 +1,3 @@
export function StepFindContactsIntro() {
export function StepFindContactsIntro(): never {
throw new Error('StepFindContactsIntro is not available on web')
}
@@ -4,6 +4,16 @@ import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
export type ValuePropositionPagerProps = {
step: 0 | 1 | 2
/**
* Only the native pager changes pages itself (by swiping); the web pager
* is driven entirely by the step prop.
*/
setStep: (step: 0 | 1 | 2) => void
avatarUri?: string
}
export function useValuePropText(step: 0 | 1 | 2) {
const {_} = useLingui()
@@ -8,17 +8,17 @@ import {useLingui} from '@lingui/react'
import {atoms as a, tokens, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {PROP_1, PROP_2, PROP_3} from './images'
import {Dot, useValuePropText} from './ValuePropositionPager.shared'
import {
Dot,
useValuePropText,
type ValuePropositionPagerProps,
} from './ValuePropositionPager.shared'
export function ValuePropositionPager({
step,
setStep,
avatarUri,
}: {
step: 0 | 1 | 2
setStep: (step: 0 | 1 | 2) => void
avatarUri?: string
}) {
}: ValuePropositionPagerProps) {
const t = useTheme()
const [activePage, setActivePage] = useState(step)
const ref = useRef<PagerView>(null)
@@ -6,15 +6,16 @@ import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {PROP_1, PROP_2, PROP_3} from './images'
import {Dot, useValuePropText} from './ValuePropositionPager.shared'
import {
Dot,
useValuePropText,
type ValuePropositionPagerProps,
} from './ValuePropositionPager.shared'
export function ValuePropositionPager({
step,
avatarUri,
}: {
step: 0 | 1 | 2
avatarUri?: string
}) {
}: ValuePropositionPagerProps) {
const t = useTheme()
const {_} = useLingui()
+1 -1
View File
@@ -34,7 +34,7 @@ function renderItem({
)
}
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
function keyExtractor(item: AppBskyActorDefs.ProfileView) {
return item.did
}
+3 -2
View File
@@ -1,5 +1,5 @@
import {useCallback, useEffect, useImperativeHandle, useState} from 'react'
import {findNodeHandle, View} from 'react-native'
import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -17,6 +17,7 @@ import {
type EmptyStateButtonProps,
} from '#/view/com/util/EmptyState'
import {type ListRef} from '#/view/com/util/List'
import {findListNativeTag} from '#/view/com/util/listNativeTag'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {atoms as a, ios, useTheme} from '#/alf'
import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
@@ -87,7 +88,7 @@ export function ProfileFeedSection({
useEffect(() => {
if (IS_IOS && isFocused && scrollElRef.current) {
const nativeTag = findNodeHandle(scrollElRef.current)
const nativeTag = findListNativeTag(scrollElRef.current)
setScrollViewTag(nativeTag)
}
}, [isFocused, scrollElRef, setScrollViewTag])

Some files were not shown because too many files have changed in this diff Show More