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:
@@ -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 }}
|
||||
@@ -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
|
||||
Executable
+50
@@ -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
|
||||
Executable
+179
@@ -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"
|
||||
@@ -0,0 +1,356 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
|
||||
const ENTITY_REPLACEMENTS = {
|
||||
'&': '&',
|
||||
''': "'",
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
}
|
||||
|
||||
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('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
}
|
||||
|
||||
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`)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+219
-78
@@ -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
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}) {
|
||||
|
||||
@@ -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(
|
||||
() => ({
|
||||
|
||||
@@ -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>({
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-1
@@ -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.')
|
||||
}
|
||||
|
||||
+17
@@ -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
|
||||
}
|
||||
+2
-16
@@ -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,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {type AppBskyFeedDefs, type ModerationDecision} from '@atproto/api'
|
||||
export enum PostEmbedViewContext {
|
||||
ThreadHighlighted = 'ThreadHighlighted',
|
||||
Feed = 'Feed',
|
||||
FeedEmbedRecordWithMedia = 'FeedEmbedRecordWithMedia',
|
||||
ChatMessage = 'ChatMessage',
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
export function useOTAUpdates() {}
|
||||
export function useApplyPullRequestOTAUpdate() {
|
||||
return {
|
||||
tryApplyUpdate: () => {},
|
||||
tryApplyUpdate: async (_channel: string) => {},
|
||||
revertToEmbedded: () => {},
|
||||
isCurrentlyRunningPullRequestDeployment: false,
|
||||
currentChannel: 'web-build',
|
||||
|
||||
@@ -4,7 +4,7 @@ export function Provider({children}: {children: React.ReactNode}) {
|
||||
return children
|
||||
}
|
||||
|
||||
const noop = () => {}
|
||||
const noop = (_scope: string) => {}
|
||||
|
||||
export function useHotkeysContext() {
|
||||
return useMemo(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()]
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,3 @@ export interface PickerOpts {
|
||||
multiple?: boolean
|
||||
maxFiles?: number
|
||||
}
|
||||
|
||||
export interface CameraOpts {
|
||||
width: number
|
||||
height: number
|
||||
freeStyleCropEnabled?: boolean
|
||||
cropperCircleOverlay?: boolean
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ export type CommonNavigatorParams = {
|
||||
ActivityPrivacySettings: undefined
|
||||
ContentAndMediaSettings: undefined
|
||||
NotificationSettings: undefined
|
||||
ActivityNotificationSettings: undefined
|
||||
InterestsSettings: undefined
|
||||
AboutSettings: undefined
|
||||
AppIconSettings: undefined
|
||||
|
||||
+149
-153
@@ -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 ""
|
||||
|
||||
|
||||
Vendored
+13
@@ -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'
|
||||
}
|
||||
Vendored
+102
@@ -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'
|
||||
}
|
||||
@@ -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,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()
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ function renderItem({
|
||||
)
|
||||
}
|
||||
|
||||
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
|
||||
function keyExtractor(item: AppBskyActorDefs.ProfileView) {
|
||||
return item.did
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user