Compare commits

...

39 Commits

Author SHA1 Message Date
Samuel Newman a9143920f0 use debugOptimized variant 2026-07-27 21:34:38 +03:00
Samuel Newman 0e63434bb4 Reenable wake-from-background Android OTA (#11298) 2026-07-27 09:20:10 -07:00
Samuel Newman eb8f5fc4bc Update sonner-native, remove patch (#11300) 2026-07-27 09:19:33 -07:00
Samuel Newman 2ad84e540f Update react-native-uitextview (#11299) 2026-07-27 09:09:48 -07:00
Samuel Newman 97c2c516bd Downbump version to v1.129.1 (#11297) 2026-07-27 07:52:10 -07:00
Samuel Newman 34827ef644 ci: only publish the per-PR OTA when the fingerprint is clean (#11287)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 07:51:55 -07:00
Samuel Newman 691619f297 fix: stop grouping all signup failures into one sentry issue (#11278)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 07:50:40 -07:00
Samuel Newman c2e5a9093c Update play store assetlinks (#11294) 2026-07-27 07:28:28 -07:00
Samuel Newman 9b89a9adba Remove unused ScrollForwarder React Native patches (#11295) 2026-07-27 13:57:46 +03:00
Tomasz Zawadzki 22d657986d Fix IllegalStateException: Required value was null. (#11293)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 03:38:03 -07:00
Samuel Newman abde10b7bd Fix tooltip layout measurement loop (#11284) 2026-07-27 03:16:31 -07:00
Austin McKinley 51bcacb20b Publish one bundle version to both OTA origins, not two (#11291)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:58:20 -07:00
Austin McKinley b662cd43b5 Fix legacy ota1 OTA upload broken by the denis cutover (#11290) 2026-07-27 00:42:38 +03:00
Samuel Newman 66ec855efc Add emoji to GitHub Actions step names for consistency (#11286)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 00:03:45 +03:00
Samuel Newman d9e76c3954 Restore PR OTA QR comment (#11285) 2026-07-26 23:07:41 +03:00
Samuel Newman be70a31f4c Fix silently-frozen OTA fingerprint baseline with artifacts (#11231)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 23:07:20 +03:00
Tomasz Zawadzki 2b9deeeb6a Migrate runOnUI/runOnJS to scheduleOnUI/scheduleOnRN (#11249) 2026-07-26 12:00:35 -07:00
Samuel Newman d5e335e575 Update version to v1.130.0 (#11274) 2026-07-25 10:58:03 +03:00
Austin McKinley 76a928a7af ci: skip the per-PR OTA job for bot authors (#11273)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 19:07:57 -07:00
Austin McKinley 9eb78c0f6c ci: publish OTA to denis (S3) + automatic per-PR previews (#11235)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 18:18:00 -07:00
Eric Bailey fb8bda861b Add new config to lingui too (#11270) 2026-07-24 16:19:11 -05:00
estrattonbailey afd267bd2b Nightly source-language update 2026-07-24 17:57:36 +00:00
DS Boyce 93cb42b9f2 Tweak advanced search UI for better localization (#11266) 2026-07-24 10:40:37 -07:00
DS Boyce 549e3d7a7d Fix issue where form controls appear above sticky headers (#11254) 2026-07-24 10:40:16 -07:00
Samuel Newman 74a89832a3 v1.129.0 release prep (#11265) 2026-07-24 09:43:38 -07:00
Samuel Newman 19b0d89313 Update react-native-uitextview (#11264) 2026-07-24 07:40:46 -07:00
Spence Pope 98e135f489 Fix native signup captcha interactions (#11263) 2026-07-24 08:46:59 -04:00
Samuel Newman fbb0ee38c2 Add hitslop to switch account ... menu (#11262) 2026-07-24 05:44:45 -07:00
Eric Bailey 7bd27e4180 Add intl note on OTA flow (#11241) 2026-07-24 03:48:55 -07:00
dependabot[bot] 07a810945d Bump github.com/hashicorp/go-retryablehttp from 0.7.5 to 0.7.7 in /bskyweb (#11257)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 03:48:39 -07:00
Samuel Newman 63b284030a Fix Maestro E2E regressions on iOS and Android (#11258) 2026-07-24 03:48:03 -07:00
Samuel Newman 3b6fbf315b Fix Maestro E2E runtime environment (#11247) 2026-07-24 03:30:10 -07:00
DS Boyce 97169dbc35 Tweak appearance of trending topics (#11253) 2026-07-24 03:29:55 -07:00
Tomasz Zawadzki 22a4958321 Migrate useScrollViewOffset to useScrollOffset (#11250) 2026-07-24 07:09:01 +03:00
pfrazee a9df815bd0 Nightly source-language update 2026-07-24 03:10:54 +00:00
Eric Bailey a53df16e82 Update API package and filter AA configs by platform (#11244) 2026-07-23 14:15:09 -05:00
Samuel Newman 4e3d1704a6 Fix scroll forwarder web entrypoint (#11245) 2026-07-23 11:17:36 -07:00
DS Boyce 723a60e979 Display post counts greater than 1,000 (#11242) 2026-07-23 10:26:47 -07:00
Spence Pope 325246ec6c Stub RN codegen native component on web (#11240) 2026-07-23 12:52:47 -04:00
147 changed files with 89233 additions and 42839 deletions
+50
View File
@@ -0,0 +1,50 @@
---
name: Setup denis CLI
description: >
Download and verify the denis OTA publish binary from the (private)
bluesky-social/tango releases and put it on PATH. Uses a short-lived GitHub
App token scoped to contents:read on tango, since the default GITHUB_TOKEN
cannot read a private repo's releases.
inputs:
release-tag:
description: denis release tag in bluesky-social/tango to download
required: true
app-id:
description: GitHub App ID for the token used to read tango releases
required: true
private-key:
description: GitHub App private key
required: true
runs:
using: composite
steps:
- name: 🔑 Mint tango read token
id: tango-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ inputs.app-id }}
private-key: ${{ inputs.private-key }}
repositories: tango
permission-contents: read
- name: ⬇️ Download and verify denis binary
shell: bash
env:
GH_TOKEN: ${{ steps.tango-token.outputs.token }}
RELEASE_TAG: ${{ inputs.release-tag }}
run: |
BIN_DIR="$RUNNER_TEMP/denis-bin"
mkdir -p "$BIN_DIR"
cd "$BIN_DIR"
gh release download "$RELEASE_TAG" \
--repo bluesky-social/tango \
--pattern denis-linux-amd64 \
--pattern denis-linux-amd64.sha256 \
--clobber
# Verify before making it executable / putting it on PATH.
sha256sum -c denis-linux-amd64.sha256
mv denis-linux-amd64 denis
chmod +x denis
echo "$BIN_DIR" >> "$GITHUB_PATH"
+1 -1
View File
@@ -136,7 +136,7 @@ wait_for_port 1986 "the E2E mock-server manager"
phase "Starting Metro"
EXPO_PUBLIC_ENV=e2e \
NODE_ENV=test \
NODE_ENV=development \
RN_SRC_EXT=e2e.ts,e2e.tsx \
pnpm exec expo start --dev-client --clear --port 8081 \
>"$artifact_dir/metro.log" 2>&1 &
@@ -21,20 +21,20 @@ jobs:
id-token: write
steps:
- name: Checkout repository
- name: ⬇️ Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Docker buildx
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log into registry ${{ env.REGISTRY }}
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
- name: 🏷️ Extract Docker metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
@@ -43,13 +43,13 @@ jobs:
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Env
- name: 📝 Env
id: env
run: |
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Build and push Docker image
- name: 🚀 Build and push Docker image
id: build-and-push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
@@ -22,20 +22,20 @@ jobs:
id-token: write
steps:
- name: Checkout repository
- name: ⬇️ Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Docker buildx
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log into registry ${{ env.REGISTRY }}
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME }}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
- name: 🏷️ Extract Docker metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
@@ -44,7 +44,7 @@ jobs:
tags: |
type=sha,enable=true,priority=100,prefix=bskyweb:,suffix=,format=long
- name: Build and push Docker image
- name: 🚀 Build and push Docker image
id: build-and-push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
@@ -21,20 +21,20 @@ jobs:
id-token: write
steps:
- name: Checkout repository
- name: ⬇️ Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Docker buildx
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log into registry ${{ env.REGISTRY }}
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
- name: 🏷️ Extract Docker metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
@@ -43,7 +43,7 @@ jobs:
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Build and push Docker image
- name: 🚀 Build and push Docker image
id: build-and-push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
@@ -21,20 +21,20 @@ jobs:
id-token: write
steps:
- name: Checkout repository
- name: ⬇️ Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Docker buildx
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log into registry ${{ env.REGISTRY }}
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
- name: 🏷️ Extract Docker metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
@@ -43,7 +43,7 @@ jobs:
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Build and push Docker image
- name: 🚀 Build and push Docker image
id: build-and-push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
@@ -21,20 +21,20 @@ jobs:
id-token: write
steps:
- name: Checkout repository
- name: ⬇️ Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Docker buildx
- name: 🔧 Setup Docker buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log into registry ${{ env.REGISTRY }}
- name: 🔑 Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.USERNAME}}
password: ${{ env.PASSWORD }}
- name: Extract Docker metadata
- name: 🏷️ Extract Docker metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
@@ -43,7 +43,7 @@ jobs:
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Build and push Docker image
- name: 🚀 Build and push Docker image
id: build-and-push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
@@ -170,22 +170,6 @@ jobs:
payload: |
{"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
-16
View File
@@ -254,22 +254,6 @@ jobs:
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
+159 -20
View File
@@ -22,11 +22,23 @@ on:
permissions:
contents: read
# denis release tag in bluesky-social/tango whose linux-amd64 binary this
# workflow downloads to publish OTA bundles. Bump this one line to roll denis.
env:
DENIS_RELEASE_TAG: denis-v0.1.1
jobs:
bundleDeploy:
if: github.repository == 'bluesky-social/social-app'
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
# id-token: write lets this job mint an OIDC token to assume the denis
# publish role; actions: read loads the fingerprint baseline artifact;
# contents: read is still needed for the checkout.
permissions:
id-token: write
actions: read
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
cancel-in-progress: true
@@ -36,7 +48,7 @@ jobs:
steps.version.outputs.version-changed }}
steps:
- name: Check for EXPO_TOKEN
- 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"
@@ -91,23 +103,69 @@ jobs:
node-version-file: package.json
cache: pnpm
- name: ⬇️ Load fingerprint baseline
id: baseline
if: ${{ (inputs.channel || 'testflight') == 'testflight' }}
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY_ID: ${{ github.repository_id }}
run: |
url=$(gh api \
"repos/${GITHUB_REPOSITORY}/actions/artifacts?name=testflight-native-fingerprint&per_page=100" \
--jq "[.artifacts[] | select(
.expired == false and
.workflow_run.head_branch == \"main\" and
.workflow_run.head_repository_id == (\$ENV.REPOSITORY_ID | tonumber)
)] | max_by(.created_at) | .archive_download_url" \
2>/dev/null || true)
if [ -n "$url" ] && [ "$url" != "null" ]; then
mkdir baseline-artifact
if curl -sSL -H "Authorization: Bearer $GH_TOKEN" -o baseline.zip "$url" \
&& unzip -q baseline.zip -d baseline-artifact; then
if jq -e '.sources | type == "array"' \
baseline-artifact/native-fingerprint.json >/dev/null; then
echo "path=baseline-artifact/native-fingerprint.json" >> "$GITHUB_OUTPUT"
else
echo "::warning::Ignoring invalid fingerprint baseline artifact."
fi
else
echo "::warning::Could not download fingerprint baseline artifact."
fi
fi
- name: 📷 Check fingerprint and install dependencies
id: fingerprint
uses: bluesky-social/github-actions/fingerprint-native@b5556913e4aef3964cfd5936d0add3fc0d809bdb # v0.2.0
uses: bluesky-social/github-actions/fingerprint-native@abc6a46eb4badf243f55bfd7d6cec42722456300 # v0.3.0
with:
profile: ${{ inputs.channel || 'testflight' }}
previous-commit-tag: ${{ inputs.runtimeVersion }}
# The recordBaseline job uploads this marker after a successful deploy;
# on the native path, that requires both builds to succeed. A missing
# marker forces native builds so they can seed the baseline safely.
baseline-fingerprint-path: ${{ steps.baseline.outputs.path }}
# Hand the full fingerprint to recordBaseline through a short-lived
# artifact. It is uploaded unconditionally but promoted to the persistent
# baseline only after both native builds succeed.
- name: 🚀 Upload native fingerprint
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: native-fingerprint-${{ github.run_id }}
path: ${{ steps.fingerprint.outputs.current-fingerprint-path }}
retention-days: 1
if-no-files-found: error
- name: 🔤 Compile translations
uses: ./.github/actions/compile-i18n
- name: Lint check
- name: 🧹 Lint check
run: pnpm lint
- name: Prettier check
- name: 💅 Prettier check
run: pnpm prettier --check .
- name: Type check
- name: 🔎 Type check
run: pnpm typecheck
- name: 🔨 Setup EAS
@@ -146,7 +204,57 @@ jobs:
SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }}
pnpm export
- name: 📦 Package Bundle and 🚀 Deploy
# Pin ONE bundle version for both publishes below. Each script used to call
# `date +%s` itself, so the same bytes reached denis and ota1 under versions
# seconds apart (observed: 1785102575 vs 1785102614). The version is part of
# the asset URL path, so each origin then served a manifest referencing a
# path only it had -- meaning a manifest fetched from one origin and assets
# fetched from the other 404. Both scripts fall back to `date +%s` when this
# is unset, so single-publisher callers are unaffected.
- name: 🔢 Pin bundle version
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
run: echo "BUNDLE_VERSION=$(date +%s)" >> "$GITHUB_ENV"
# denis on EKS has been the sole origin for updates.bsky.app since
# 2026-07-26, so it publishes FIRST: it is the path that actually serves
# clients. The legacy ota1 upload runs after it, and exists only so that
# rolling the Bunny origin back to ota1 would find current bundles there.
#
# The ordering is load-bearing, not cosmetic. While the legacy step ran
# first, its failure skipped these steps and nothing reached EITHER origin
# -- the dual-write took down the working path with it. Both steps are
# still required to pass, so a stale ota1 remains a loud failure, but the
# publish that serves users has already landed before the legacy one can
# fail.
#
# Both halves are removed together when ota1 is decommissioned (Phase 5).
- name: ☁️ Configure AWS credentials (denis)
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
with:
role-to-assume: arn:aws:iam::007404326489:role/denis-ci-publish
aws-region: us-east-2
- name: ⬇️ Setup denis CLI
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
uses: ./.github/actions/setup-denis
with:
release-tag: ${{ env.DENIS_RELEASE_TAG }}
app-id: ${{ vars.SYNC_INTERNAL_APP_ID }}
private-key: ${{ secrets.SYNC_INTERNAL_PK }}
- name: 🚀 Publish OTA to denis (S3)
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
run: pnpm use-build-number bash scripts/denisPublish.sh
env:
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
- name: 📦 Package Bundle and 🚀 Deploy (legacy ota1)
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
run: pnpm use-build-number bash scripts/bundleUpdate.sh
@@ -155,20 +263,6 @@ jobs:
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ !steps.fingerprint.outputs.includes-changes &&
!steps.version.outputs.version-changed }}
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
buildIfNecessaryIOS:
name: Build and Submit iOS
needs: [bundleDeploy]
@@ -230,3 +324,48 @@ jobs:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
# Advance the fingerprint baseline only after BOTH native builds have shipped
# the new native surface. This replaces the old actions/cache baseline, which
# only advanced on cache eviction and so silently froze - freezing meant every
# fingerprint looked changed and OTA updates stopped deploying entirely.
#
# On the native-build path, this runs only after both builds succeed. A
# successful OTA deploy also records its fingerprint, refreshing the
# persistent marker's retention without changing the native baseline.
#
# The persistent artifact replaces the old actions/cache marker without
# requiring a PAT or mutable repository variable. Each successful deploy adds
# an immutable marker; the next run reads the newest non-expired one using the
# built-in GITHUB_TOKEN.
recordBaseline:
name: Record fingerprint baseline
runs-on: ubuntu-latest
needs: [bundleDeploy, buildIfNecessaryIOS, buildIfNecessaryAndroid]
if: ${{ always() &&
(inputs.channel || 'testflight') == 'testflight' &&
needs.bundleDeploy.result == 'success' &&
(needs.bundleDeploy.outputs.changes-detected != 'true' ||
(needs.buildIfNecessaryIOS.result == 'success' &&
needs.buildIfNecessaryAndroid.result == 'success')) &&
github.repository == 'bluesky-social/social-app' }}
permissions:
actions: read
steps:
- name: ⬇️ Download native fingerprint
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: native-fingerprint-${{ github.run_id }}
- name: 🧐 Validate native fingerprint
run: >
jq -e '.sources | type == "array"' native-fingerprint.json >/dev/null ||
(echo "::error::native fingerprint artifact was invalid; refusing to record it as the baseline." && exit 1)
- name: 🚀 Record fingerprint baseline
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: testflight-native-fingerprint
path: native-fingerprint.json
retention-days: 90
if-no-files-found: error
+3 -3
View File
@@ -53,18 +53,18 @@ jobs:
cancel-in-progress: false
steps:
- name: Checkout repository
- name: ⬇️ Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
- name: Configure AWS credentials (OIDC)
- name: ☁️ Configure AWS credentials (OIDC)
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
- name: 🤖 Claude
uses: anthropics/claude-code-action@e90deca47693f9457b72f2b53c17d7c445a87342 # v1.0.171
with:
use_bedrock: 'true'
+3 -3
View File
@@ -39,18 +39,18 @@ jobs:
cancel-in-progress: true
steps:
- name: Checkout repository
- name: ⬇️ Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
- name: Configure AWS credentials (OIDC)
- name: ☁️ Configure AWS credentials (OIDC)
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
- name: 🤖 Claude review
uses: anthropics/claude-code-action@e90deca47693f9457b72f2b53c17d7c445a87342 # v1.0.171
with:
use_bedrock: 'true'
+10 -10
View File
@@ -17,32 +17,32 @@ jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
- name: ⬇️ Git Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go tooling
- name: 🔧 Set up Go tooling
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: bskyweb/go.mod
cache-dependency-path: bskyweb/go.sum
- name: Dummy Static Files
- name: 📄 Dummy Static Files
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
- name: Check
- name: 🔍 Check
run: cd bskyweb/ && make check
- name: Build (binary)
- name: 🏗️ Build (binary)
run: cd bskyweb/ && make build
- name: Test
- name: 🧪 Test
run: cd bskyweb/ && make test
lint:
runs-on: ubuntu-latest
steps:
- name: Git Checkout
- name: ⬇️ Git Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go tooling
- name: 🔧 Set up Go tooling
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: bskyweb/go.mod
cache-dependency-path: bskyweb/go.sum
- name: Dummy Static Files
- name: 📄 Dummy Static Files
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
- name: Lint
- name: 🧹 Lint
run: cd bskyweb/ && make lint
+13 -13
View File
@@ -24,9 +24,9 @@ jobs:
job:
[lint, prettier, 'typecheck:ios', 'typecheck:android', 'typecheck:web']
steps:
- name: Check out Git repository
- name: ⬇️ Check out Git repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Verify Node version pins match package.json
- name: 🔍 Verify Node version pins match package.json
run: |
set -euo pipefail
expected=$(node -p "require('./package.json').engines.node.replace(/[^0-9.]/g, '')")
@@ -52,16 +52,16 @@ jobs:
check "eas.json" "$v"
exit $rc
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
- name: pnpm install
- name: 📦 pnpm install
run: pnpm install --frozen-lockfile
- name: Check & compile i18n
- name: 🔤 Check & compile i18n
run: pnpm intl:build
- name: Lint checks
- name: 🧹 Lint checks
run: pnpm ${{ matrix.job }}
# Aggregates the matrix results into a single stable check name so branch
# protection can require "Run linters" regardless of how many matrix jobs run.
@@ -73,7 +73,7 @@ jobs:
needs: [linting]
runs-on: ubuntu-latest
steps:
- name: Require linting to have succeeded
- name: Require linting to have succeeded
env:
RESULT: ${{ needs.linting.result }}
run: |
@@ -87,19 +87,19 @@ jobs:
matrix:
shard: [1, 2, 3, 4]
steps:
- name: Check out Git repository
- name: ⬇️ Check out Git repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
- name: pnpm install
- name: 📦 pnpm install
run: pnpm install --frozen-lockfile
- name: Check & compile i18n
- name: 🔤 Check & compile i18n
run: pnpm intl:build
- name: Run tests
- name: 🧪 Run tests
run: |
NODE_ENV=test pnpm test --forceExit --shard=${{ matrix.shard }}/${{ strategy.job-total }}
# Aggregates the sharded test results into a single stable check name so branch
@@ -112,7 +112,7 @@ jobs:
needs: [testing]
runs-on: ubuntu-latest
steps:
- name: Require testing to have succeeded
- name: Require testing to have succeeded
env:
RESULT: ${{ needs.testing.result }}
run: |
+38 -38
View File
@@ -28,41 +28,41 @@ jobs:
runs-on: macos-26-xlarge
timeout-minutes: 120
steps:
- name: Checkout
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Select Xcode 26.4
- name: 🛠️ Select Xcode 26.4
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: "26.4"
- name: Prepare E2E configuration
- name: 🧰 Prepare E2E configuration
run: |
mkdir -p artifacts/ios
echo "Installing dependencies" > artifacts/ios/phase.txt
cp .env.example .env.test
cp .env.example .env.development
cp google-services.json.example google-services.json
- name: Set up Expo project
- name: 🔧 Set up Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: Set up Java 17
- name: ☕️ Set up Java 17
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: temurin
java-version: "17"
- name: Install dev-env dependencies
- name: 📦 Install dev-env dependencies
run: pnpm --dir dev-env install --frozen-lockfile 2>&1 | tee artifacts/ios/dependencies.log
- name: Compile translations
- name: 🔤 Compile translations
uses: ./.github/actions/compile-i18n
- name: Install Maestro 2.6.1
- name: 📥 Install Maestro 2.6.1
run: |
echo "Installing Maestro" > artifacts/ios/phase.txt
curl -fsSL -o "$RUNNER_TEMP/maestro.zip" \
@@ -74,7 +74,7 @@ jobs:
"$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
- name: 📱 Boot one iOS simulator
run: |
echo "Booting iOS simulator" > artifacts/ios/phase.txt
device_name="iPhone 17"
@@ -112,10 +112,10 @@ jobs:
xcrun simctl bootstatus "$udid" -b
echo "Using $device_name on $runtime_name ($udid)"
- name: Mark iOS development client build phase
- name: 🏷️ Mark iOS development client build phase
run: echo "Building the iOS development client" > artifacts/ios/phase.txt
- name: Build iOS development client
- name: 🏗️ Build iOS development client
uses: ./.github/actions/eas-local-build
with:
platform: ios
@@ -123,7 +123,7 @@ jobs:
output: ${{ runner.temp }}/nightly-e2e-ios.tar.gz
log-path: artifacts/ios/build.log
- name: Install iOS development client
- name: 📲 Install iOS development client
run: |
build_contents="$RUNNER_TEMP/nightly-e2e-ios-build"
mkdir -p "$build_contents"
@@ -135,14 +135,14 @@ jobs:
fi
xcrun simctl install "$IOS_UDID" "$app_path" 2>&1 | tee -a artifacts/ios/build.log
- name: Run iOS Maestro suite
- name: 🧪 Run iOS Maestro suite
run: .github/scripts/run-nightly-e2e.sh ios "$IOS_UDID"
- name: Clean up iOS services and simulator
- name: 🧹 Clean up iOS services and simulator
if: always()
run: .github/scripts/cleanup-nightly-e2e.sh ios "${IOS_UDID:-}"
- name: Upload iOS E2E artifacts
- name: 🚀 Upload iOS E2E artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -158,36 +158,36 @@ jobs:
runs-on: Linux-x64-32core
timeout-minutes: 120
steps:
- name: Checkout
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Prepare E2E configuration
- name: 🧰 Prepare E2E configuration
run: |
mkdir -p artifacts/android
echo "Installing dependencies" > artifacts/android/phase.txt
cp .env.example .env.test
cp .env.example .env.development
cp google-services.json.example google-services.json
- name: Set up Expo project
- name: 🔧 Set up Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: Set up Java 17
- name: ☕️ Set up Java 17
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
distribution: temurin
java-version: "17"
- name: Install dev-env dependencies
- name: 📦 Install dev-env dependencies
run: pnpm --dir dev-env install --frozen-lockfile 2>&1 | tee artifacts/android/dependencies.log
- name: Compile translations
- name: 🔤 Compile translations
uses: ./.github/actions/compile-i18n
- name: Install Maestro 2.6.1
- name: 📥 Install Maestro 2.6.1
run: |
echo "Installing Maestro" > artifacts/android/phase.txt
curl -fsSL -o "$RUNNER_TEMP/maestro.zip" \
@@ -199,7 +199,7 @@ jobs:
"$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
- 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}}"
@@ -299,10 +299,10 @@ jobs:
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
- name: 🏷️ Mark Android development client build phase
run: echo "Building the Android development client" > artifacts/android/phase.txt
- name: Build Android development client
- name: 🏗️ Build Android development client
uses: ./.github/actions/eas-local-build
with:
platform: android
@@ -310,15 +310,15 @@ jobs:
output: ${{ runner.temp }}/nightly-e2e-android.apk
log-path: artifacts/android/build.log
- name: Install Android development client
- 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
- name: 🧪 Run Android Maestro suite
run: .github/scripts/run-nightly-e2e.sh android emulator-5554
- name: Capture emulator crash diagnostics
- name: 🩺 Capture emulator crash diagnostics
if: always()
run: |
{
@@ -338,11 +338,11 @@ jobs:
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
- name: 🧹 Clean up Android services and emulator
if: always()
run: .github/scripts/cleanup-nightly-e2e.sh android emulator-5554
- name: Upload Android E2E artifacts
- name: 🚀 Upload Android E2E artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -360,26 +360,26 @@ jobs:
actions: read
contents: read
steps:
- name: Checkout
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Download iOS artifacts
- 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
- 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
- name: 🔗 Resolve artifact links
env:
GH_TOKEN: ${{ github.token }}
run: |
@@ -395,7 +395,7 @@ jobs:
}' artifact-response.json > artifact-links.json
fi
- name: Summarize platform results
- name: 📊 Summarize platform results
id: summary
env:
IOS_STATUS: ${{ needs.ios.result }}
@@ -415,7 +415,7 @@ jobs:
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
- name: 🔔 Notify Slack of E2E failures
if: steps.summary.outputs.notify == 'true'
uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5
with:
@@ -15,26 +15,26 @@ jobs:
contents: write
steps:
- name: Check out Git repository
- name: ⬇️ Check out Git repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ssh-key: ${{secrets.GH_ACTION_DEPLOY_KEY}}
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
cache: pnpm
- name: pnpm install
- name: 📦 pnpm install
run: pnpm install --frozen-lockfile
- name: Extract language strings
- name: 🔤 Extract language strings
run: pnpm intl:extract
- name: Create commit
- name: 📝 Create commit
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
with:
commit_message: Nightly source-language update
file_pattern: ./src/locale/locales/en/messages.po
- name: Push source lang to Crowdin
- name: 🚀 Push source lang to Crowdin
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
with:
upload_sources: true
-221
View File
@@ -1,221 +0,0 @@
---
name: PR Comment Trigger
on:
issue_comment:
types: [created]
# Permissions are granted per-job below; anything unlisted defaults to none
permissions: {}
jobs:
handle-comment:
if: github.event.issue.pull_request
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
should-deploy: ${{ steps.check-org.outputs.result }}
steps:
- name: Check if bot is mentioned
id: check-mention
env:
COMMENT: ${{ github.event.comment.body }}
run: |
if [[ "$COMMENT" == *"@github-actions"* ]] || \
[[ "$COMMENT" == *"github-actions[bot]"* ]]; then
bot_mentioned=true
else
bot_mentioned=false
fi
if [[ "$COMMENT" == *"ota"* ]]; then
has_ota=true
else
has_ota=false
fi
if [[ "$bot_mentioned" == "true" ]] && [[ "$has_ota" == "true" ]]; then
echo "mentioned=true" >> $GITHUB_OUTPUT
else
echo "mentioned=false" >> $GITHUB_OUTPUT
fi
- name: Check commenter has write access
if: steps.check-mention.outputs.mentioned == 'true'
id: check-org
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
try {
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.payload.comment.user.login
});
const hasAccess = ['admin', 'write'].includes(perm.permission);
console.log(`User has ${perm.permission} access`);
return hasAccess;
} catch(error) {
console.log('User has no repository access');
return false;
}
bundle-deploy:
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
needs: [handle-comment]
if: needs.handle-comment.outputs.should-deploy == 'true'
permissions:
contents: read
pull-requests: write
steps:
- name: Get PR HEAD SHA
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
id: pr-info
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: process.env.ISSUE_NUMBER,
});
// This workflow runs with repo secrets in scope, so never build
// code from a fork: the commenter authorizes the deploy, but a
// fork controls what code would run during it
const expected = `${context.repo.owner}/${context.repo.repo}`;
const head = pr.data.head.repo?.full_name;
if (head !== expected) {
core.setFailed(`OTA deploys are only allowed for branches in ${expected}, not forks (got ${head})`);
return;
}
console.log(`PR HEAD SHA: ${pr.data.head.sha}`);
console.log(`PR HEAD REF: ${pr.data.head.ref}`);
core.setOutput('head-sha', pr.data.head.sha);
core.setOutput('head-ref', pr.data.head.ref);
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
number: ${{ github.event.issue.number }}
message: |
An OTA deployment has been requested and is now running for `${{ steps.pr-info.outputs.head-sha }}`.
[Here is some music to listen to while you wait...](https://www.youtube.com/watch?v=VBlFHuCzPgY)
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
- 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:
ref: ${{ steps.pr-info.outputs.head-sha }}
- 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: Install dependencies
run: pnpm install --frozen-lockfile
- 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
- name: Lint check
run: pnpm lint
- name: Type check
run: pnpm typecheck
- 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: 🪛 Setup jq
uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1
- name: Env
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_ENV=testflight" >> .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: 🏗️ Create Bundle
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 export
- name: 📦 Package Bundle and 🚀 Deploy
run: pnpm use-build-number bash scripts/bundleUpdate.sh
env:
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
CHANNEL_NAME: pull-request-${{ github.event.issue.number }}
RUNTIME_VERSION:
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
with:
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
number: ${{ github.event.issue.number }}
message: |
Your requested OTA deployment was successful! You may now apply it by either scanning the QR code or opening the deep link below in your browser:
<img src="https://bsky-qr.vercel.app?channel=pull-request-$ISSUE_NUMBER" width=300 height=300>
`bluesky://intent/apply-ota?channel=pull-request-${{ github.event.issue.number }}`
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
if: failure()
with:
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
number: ${{ github.event.issue.number }}
message: |
Your requested OTA deployment was unsuccessful. See action logs for more details.
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
+150 -5
View File
@@ -15,9 +15,14 @@ concurrency:
# Permissions are granted per-job below; anything unlisted defaults to none.
# pull-requests: write is needed by sticky-pull-request-comment to post the
# bundle-size and fingerprint diffs
# bundle-size and fingerprint diffs and the PR OTA install link
permissions: {}
# denis release tag in bluesky-social/tango whose linux-amd64 binary the PR OTA
# job downloads. Bump this one line to roll denis.
env:
DENIS_RELEASE_TAG: denis-v0.1.1
jobs:
# Populate this from main so every PR can restore the same trusted baseline.
webpack-analyzer-base:
@@ -78,7 +83,7 @@ jobs:
node-version-file: package.json
cache: pnpm
- name: Ensure tracking relevant branches and checkout base
- name: 🌿 Ensure tracking relevant branches and checkout base
env:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
@@ -86,13 +91,13 @@ jobs:
git checkout $HEAD_REF
git checkout $BASE_REF
- name: Get the base commit
- name: 🔍 Get the base commit
id: base-commit
env:
BASE_REF: ${{ github.base_ref }}
run: echo base-commit=$(git log -n 1 $BASE_REF --pretty=format:'%H') >> "$GITHUB_OUTPUT"
- name: Merge PR commit
- name: 🔀 Merge PR commit
env:
HEAD_REF: ${{ github.head_ref }}
run: |
@@ -116,7 +121,7 @@ jobs:
path: stats.json
key: stats-base-main-${{ steps.base-commit.outputs.base-commit }}
- name: Restore to base commit
- name: Restore to base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
env:
BASE_COMMIT: ${{ steps.base-commit.outputs.base-commit }}
@@ -155,6 +160,10 @@ jobs:
permissions:
contents: read
pull-requests: write
outputs:
# Empty when the native surface is unchanged, 'true' when it changed.
# publish-pr-ota gates on this.
includes-changes: ${{ steps.fingerprint.outputs.includes-changes }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -205,6 +214,16 @@ jobs:
header: fingerprint-diff
delete: true
# publish-pr-ota is skipped once the fingerprint changes, so any install
# link left over from an earlier fingerprint-clean commit on this PR now
# points at a bundle that no longer matches the PR. Drop it.
- name: 💬 Delete stale OTA install comment
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
if: ${{ steps.fingerprint.outputs.includes-changes }}
with:
header: pull-request-ota
delete: true
- name: 🏷️ Label as fingerprint changed
if: ${{ steps.fingerprint.outputs.includes-changes }}
env:
@@ -220,3 +239,129 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh pr edit "$PR_NUMBER" --remove-label "bot: fingerprint changed" || true
# Automatic per-PR OTA preview, published to the pull-request-<N> channel on
# denis. Replaces the old `@github-actions ota` comment trigger. Gated to
# same-repo PRs (fork guard): a branch can only exist in this repo if someone
# with write access pushed it, so an outside contributor (who can only open a
# PR from a fork) never runs this job with the denis publish role in scope.
# This matches the fork-guard gate the other jobs in this workflow use;
# author_association is deliberately NOT checked (it can't identify a private
# org member and would skip their PRs).
#
# Bot authors are excluded: Dependabot pushes in-repo branches, so it passes
# the fork guard, but GitHub withholds repo secrets from Dependabot-triggered
# runs. EXPO_TOKEN is then empty and the job fails at setup — a red check on
# every dependabot PR. There is no OTA preview worth publishing for a
# dependency bump anyway.
#
# Gated on a clean fingerprint-native run: an OTA can only carry JS, so once
# the native surface changes the published bundle no longer represents the PR
# and installing it on a store/TestFlight client is misleading at best. Those
# PRs need a native build instead. A skipped or failed fingerprint job also
# skips this one - without a verdict we can't say the OTA is representative.
publish-pr-ota:
name: Publish PR OTA to denis
needs: fingerprint-native
runs-on: ubuntu-latest
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.type != 'Bot' &&
needs.fingerprint-native.outputs.includes-changes != 'true'
concurrency:
group: pr-ota-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
id-token: write
contents: read
steps:
- name: ⬇️ Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: 🛠️ Setup Expo project
uses: ./.github/actions/setup-expo-project
with:
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: 🔤 Compile translations
uses: ./.github/actions/compile-i18n
- name: ✏️ Write environment variables
id: env
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: testflight
- name: 🏗️ Create Bundle
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.release-version }}
SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }}
pnpm export
- name: ☁️ Configure AWS credentials (denis, PR-scoped)
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
with:
role-to-assume: arn:aws:iam::007404326489:role/denis-ci-publish-pr
aws-region: us-east-2
# Defense-in-depth: the base role is already scoped to pr/*, but narrow
# this session further to just THIS PR's prefix so a bug can't write to
# another PR's objects or the prod tree.
inline-session-policy: |-
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::bsky-denis-ota-prod/pr/${{ github.event.pull_request.number }}/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::bsky-denis-ota-prod",
"Condition": {
"StringLike": { "s3:prefix": "pr/${{ github.event.pull_request.number }}/*" }
}
}
]
}
- name: ⬇️ Setup denis CLI
uses: ./.github/actions/setup-denis
with:
release-tag: ${{ env.DENIS_RELEASE_TAG }}
app-id: ${{ vars.SYNC_INTERNAL_APP_ID }}
private-key: ${{ secrets.SYNC_INTERNAL_PK }}
- name: 🚀 Publish OTA to denis (S3)
run: pnpm use-build-number bash scripts/denisPublish.sh
env:
RUNTIME_VERSION: ''
CHANNEL_NAME: pull-request-${{ github.event.pull_request.number }}
comment-pr-ota:
name: Comment PR OTA install link
needs: publish-pr-ota
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: 💬 Drop OTA install comment
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: pull-request-ota
message: |
The OTA deployment for this PR was successful! You may now apply it by either scanning the QR code or opening the deep link below in your browser:
<img src="https://bsky-qr.vercel.app?channel=pull-request-${{ github.event.pull_request.number }}" width="300" height="300" alt="QR code for the PR OTA deployment">
`bluesky://intent/apply-ota?channel=pull-request-${{ github.event.pull_request.number }}`
+3 -3
View File
@@ -13,14 +13,14 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'bluesky-social/social-app'
steps:
- name: Checkout public repo
- name: ⬇️ Checkout public repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
# Don't persist the checkout auth header; the push below authenticates
# with the app token embedded in the remote URL instead
persist-credentials: false
- name: Generate GitHub App Token
- name: 🔑 Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
@@ -33,7 +33,7 @@ jobs:
# .github/workflows/, which GitHub refuses to push without it.
permission-contents: write
permission-workflows: write
- name: Push to internal repo
- name: 🚀 Push to internal repo
env:
TOKEN: ${{ steps.app-token.outputs.token }}
run: |
+6 -6
View File
@@ -14,33 +14,33 @@ jobs:
name: No manual pnpm-lock.yaml edits
runs-on: ubuntu-latest
steps:
- name: Check out PR HEAD
- name: ⬇️ Check out PR HEAD
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Fetch base branch
- name: ⬇️ Fetch base branch
env:
BASE_REF: ${{ github.base_ref }}
run: git fetch origin $BASE_REF --depth=1
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Install node
- name: 🔧 Install node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
- name: Reset pnpm-lock.yaml to base
- name: Reset pnpm-lock.yaml to base
env:
BASE_REF: ${{ github.base_ref }}
run: git show "origin/$BASE_REF:pnpm-lock.yaml" > pnpm-lock.yaml
- name: pnpm install
- name: 📦 pnpm install
# Fine to skip scripts since we don't run any code
run: pnpm clean && pnpm install --ignore-scripts --no-frozen-lockfile
- name: Verify pnpm-lock.yaml
- name: 🔍 Verify pnpm-lock.yaml
run: |
git diff --quiet --exit-code || {
echo '::error::`pnpm-lock.yaml` does not match what pnpm would generate given the base `pnpm-lock.yaml` and the head `package.json`.'
+2 -2
View File
@@ -20,12 +20,12 @@ jobs:
name: Audit workflows with zizmor
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
- name: ⬇️ Check out Git repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run zizmor
- name: 🛡️ Run zizmor
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
with:
# Annotate the PR directly instead of uploading SARIF to the
+3
View File
@@ -134,3 +134,6 @@ bskyweb/static/media/*.svg
# superpowers plugin plans/specs — local-only workspace
docs/superpowers/
.claude/worktrees
# pnpm
.pnpm-store/
+13 -11
View File
@@ -1,6 +1,8 @@
/**
* @param {import("@babel/core").ConfigAPI} api
* @returns {import("@babel/core").InputOptions}
*/
module.exports = function (api) {
api.cache(true)
const isTestEnv = process.env.NODE_ENV === 'test'
return {
presets: [
[
@@ -10,7 +12,7 @@ module.exports = function (api) {
native: {
// Disable ESM -> CJS compilation because Metro takes care of it.
// However, we need it in Jest tests since those run without Metro.
disableImportExportTransform: !isTestEnv,
disableImportExportTransform: !api.env('test'),
},
},
],
@@ -29,15 +31,15 @@ module.exports = function (api) {
},
},
],
// cannot use `env` field because it will put them after
// the `react-native-worklets/plugin` plugin
...(api.env('test')
? ['@babel/plugin-transform-class-static-block']
: []),
...(api.env('production') ? ['transform-remove-console'] : []),
'react-native-worklets/plugin', // NOTE: this plugin MUST be last
],
env: {
production: {
plugins: ['transform-remove-console'],
},
test: {
plugins: ['@babel/plugin-transform-class-static-block'],
},
},
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
+6 -5
View File
@@ -17,6 +17,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etly
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg=
github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
@@ -40,10 +42,10 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
@@ -186,7 +188,6 @@ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+8 -3
View File
@@ -1,11 +1,16 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "xyz.blueskyweb.app",
"sha256_cert_fingerprints":
["C1:4D:3C:6B:B5:D6:D9:AE:CF:C5:0B:BC:C1:9B:29:6D:D4:E6:87:46:36:D5:4C:1A:64:1C:14:08:BF:7E:F9:62", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
"sha256_cert_fingerprints": [
"C1:4D:3C:6B:B5:D6:D9:AE:CF:C5:0B:BC:C1:9B:29:6D:D4:E6:87:46:36:D5:4C:1A:64:1C:14:08:BF:7E:F9:62",
"FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"
]
}
}
]
+3 -1
View File
@@ -11,7 +11,9 @@
"two_letters_code": {
"pt": "pt-PT",
"pt-BR": "pt-BR",
"en-CA": "en-CA",
"en-GB": "en-GB",
"fr-CA": "fr-CA",
"zh-CN": "zh-CN",
"zh-TW": "zh-TW",
"zh-HK": "zh-HK"
@@ -19,4 +21,4 @@
}
}
]
}
}
+16 -5
View File
@@ -52,7 +52,18 @@ have already been deployed for this release, incremement the branch name e.g.
Cherry pick in the commits that need to be deployed on top of the most recent
release or OTA.
### 5. Manually set build numbers
### 5. Pull translations
Since translators may have added new strings, and positions within the code may
have shifted, it's typically best to pull the latest translations.
Run this and commit the result as the last commit on the OTA branch.
```sh
pnpm intl:release
```
### 6. Manually set build numbers
Log in to the EAS CLI with `eas login` and manually set the build numbers to the
values you found in **Step 1**.
@@ -75,7 +86,7 @@ values you found in **Step 1**.
👉 **Save the previous values,** in this case `1011` and `641`, so you can reset
them after the OTA completes.
### 6. Run the GitHub actions
### 7. Run the GitHub actions
You'll need to run two separate actions: one to deploy the iOS/Android OTA
itself, and one to build the web Docker container.
@@ -102,13 +113,13 @@ and run the action.
| ----- | --- |
| Select your OTA branch `1.x.0-ota-x` and click "Run workflow" | ![workflow](./img/web_action.png) |
### 7. Deploy web
### 8. Deploy web
Once the web Docker container build finishes, go to your `1.x.0-ota-x` branch,
copy the most recent commit hash. Post this hash in `#ops-deploys` and request
someone with web deploy access deploy the built container.
### 8. Confirm successful deployment
### 9. Confirm successful deployment
In about five minutes, the new deployment should be deployed and devices will
begin downloading and installing in the background.
@@ -119,7 +130,7 @@ build from your device and re-install from the App Store. Then, you'll need to:
- Quit and reopen the app
- Check the `Settings > About` page and confirm the hash matches the most recent hash on your OTA branch
### 9. Reset build numbers
### 10. Reset build numbers
Grab the build numbers you saved in **Step 5** and reverse the EAS CLI commands
to reset the build numbers.
+1 -1
View File
@@ -28,7 +28,7 @@
},
"env": {
"EXPO_PUBLIC_ENV": "e2e",
"NODE_ENV": "test",
"NODE_ENV": "development",
"RN_SRC_EXT": "e2e.ts,e2e.tsx"
}
},
+2
View File
@@ -13,11 +13,13 @@ export default defineConfig({
'de',
'el',
'en-GB',
'en-CA',
'eo',
'es',
'eu',
'fi',
'fr',
'fr-CA',
'fy',
'ga',
'gd',
@@ -0,0 +1 @@
export {ScrollForwarderView} from './ScrollForwarderView'
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.129.0",
"version": "1.129.1",
"private": true,
"engines": {
"node": ">=24.18.0"
@@ -40,7 +40,7 @@
"prepare": "is-ci || husky",
"postinstall": "pnpm intl:compile-if-needed",
"prebuild": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean",
"android": "expo run:android",
"android": "expo run:android --variant debugOptimized",
"android:prod": "expo run:android --variant release",
"android:profile": "BSKY_PROFILE=1 expo run:android --variant release",
"ios": "expo run:ios",
@@ -67,9 +67,9 @@
"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",
"e2e:start": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo start -c",
"e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=development RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
"e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=development RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
"e2e:start": "EXPO_PUBLIC_ENV=e2e NODE_ENV=development RN_SRC_EXT=e2e.ts,e2e.tsx expo start -c",
"e2e:run": "maestro test",
"perf:test": "NODE_ENV=test maestro test",
"perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml",
@@ -96,7 +96,7 @@
"prettier": "prettier --check ."
},
"dependencies": {
"@atproto/api": "0.20.32",
"@atproto/api": "0.20.33",
"@atproto/common-web": "0.5.6",
"@atproto/syntax": "0.7.2",
"@bitdrift/react-native": "^0.6.8",
@@ -108,6 +108,7 @@
"@bsky.app/expo-scroll-edge-effect": "^0.1.9",
"@bsky.app/expo-translate-text": "^0.2.9",
"@bsky.app/peek-menu": "^0.3.1",
"@bsky.app/react-native-uitextview": "^2.5.0",
"@bsky.app/sift": "^0.3.9",
"@bsky.app/tapper": "^0.6.1",
"@bsky.app/video": "0.3.6",
@@ -237,7 +238,6 @@
"react-native-screens": "4.24.0",
"react-native-scroll-forwarder": "link:./modules/react-native-scroll-forwarder",
"react-native-svg": "15.12.1",
"react-native-uitextview": "^2.2.0",
"react-native-uuid": "^2.0.3",
"react-native-view-shot": "^4.0.3",
"react-native-web": "^0.21.0",
@@ -250,7 +250,7 @@
"setimmediate": "^1.0.5",
"slugify": "^1.6.9",
"sonner": "^2.0.7",
"sonner-native": "0.21.0",
"sonner-native": "0.26.4",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"tldts": "^6.1.46",
+34 -54
View File
@@ -23,19 +23,6 @@ index c593d9ee2155a826352ebca34845aa5792b2eec3..3c26cd737f21116ff0aa48190e97e6c0
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
index 914a2494a57923fbf185644b7e2bb8aca8848e56..0deac55f22350f5e8377d8963fb1c2434bf6abfd 100644
--- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
+++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
@@ -19,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN
*/
@interface RCTPullToRefreshViewComponentView : RCTViewComponentView <RCTCustomPullToRefreshViewProtocol>
+- (void)beginRefreshingProgrammatically;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
index 0d231bc8aa938da296eb3b981e8ac9595a43b87f..be0a10d9c4de1892fa00bcbf8d63d739b66d8ffe 100644
--- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.mm
@@ -60,7 +47,7 @@ index 0d231bc8aa938da296eb3b981e8ac9595a43b87f..be0a10d9c4de1892fa00bcbf8d63d739
if (newConcreteProps.tintColor != oldConcreteProps.tintColor) {
diff --git a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
index 1494fd225aff1fa0429e917404d6b4ca5fc961c5..682e41b141c38c830bbcd9f2ce0de07b18f13977 100644
index 1494fd225aff1fa0429e917404d6b4ca5fc961c5..d0cce700090245444f8ce51e517d5ceca09526f6 100644
--- a/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
+++ b/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
@@ -380,7 +380,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
@@ -102,20 +89,18 @@ index 1494fd225aff1fa0429e917404d6b4ca5fc961c5..682e41b141c38c830bbcd9f2ce0de07b
Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
diff --git a/React/Views/RefreshControl/RCTRefreshControl.h b/React/Views/RefreshControl/RCTRefreshControl.h
index e9b330fa7c29c42653a3b0191d0f8a1b13b2d3de..ec5f58c887bfd949f1279ef1c31352e0b465e9ec 100644
index e9b330fa7c29c42653a3b0191d0f8a1b13b2d3de..5fbb2e05cadfc06fd7a18bf52b81bc399e92f3ca 100644
--- a/React/Views/RefreshControl/RCTRefreshControl.h
+++ b/React/Views/RefreshControl/RCTRefreshControl.h
@@ -15,5 +15,8 @@
@@ -15,5 +15,6 @@
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) RCTDirectEventBlock onRefresh;
@property (nonatomic, weak) UIScrollView *scrollView;
+@property (nonatomic, copy) UIColor *customTintColor;
+
+- (void)forwarderBeginRefreshing;
@end
diff --git a/React/Views/RefreshControl/RCTRefreshControl.m b/React/Views/RefreshControl/RCTRefreshControl.m
index 53bfd04703502d5b8e932c47a528bb03cd79d330..ff1b1ed5e060bcf0d91528c6d3c2c5c8acf24967 100644
index 53bfd04703502d5b8e932c47a528bb03cd79d330..e2e0c9f4e5d1a3a3b178a7ec69aa63e3039b6dec 100644
--- a/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/React/Views/RefreshControl/RCTRefreshControl.m
@@ -23,6 +23,7 @@ @implementation RCTRefreshControl {
@@ -139,7 +124,7 @@ index 53bfd04703502d5b8e932c47a528bb03cd79d330..ff1b1ed5e060bcf0d91528c6d3c2c5c8
- (void)didMoveToWindow
{
[super didMoveToWindow];
@@ -221,4 +228,50 @@ - (void)refreshControlValueChanged
@@ -221,4 +228,16 @@ - (void)refreshControlValueChanged
}
}
@@ -154,40 +139,6 @@ index 53bfd04703502d5b8e932c47a528bb03cd79d330..ff1b1ed5e060bcf0d91528c6d3c2c5c8
+ [super setTintColor:tintColor];
+ }
+}
+
+// This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native
+// libraries to perform a refresh of a scrollview and access the refresh control's onRefresh
+// function.
+- (void)forwarderBeginRefreshing
+{
+ _refreshingProgrammatically = NO;
+
+ [self sizeToFit];
+
+ if (!self.scrollView) {
+ return;
+ }
+
+ UIScrollView *scrollView = (UIScrollView *)self.scrollView;
+
+ [UIView animateWithDuration:0.3
+ delay:0
+ options:UIViewAnimationOptionBeginFromCurrentState
+ animations:^(void) {
+ // Whenever we call this method, the scrollview will always be at a position of
+ // -130 or less. Scrolling back to -65 simulates the default behavior of RCTRefreshControl
+ [scrollView setContentOffset:CGPointMake(0, -65)];
+ }
+ completion:^(__unused BOOL finished) {
+ [super beginRefreshing];
+ [self setCurrentRefreshingState:super.refreshing];
+
+ if (self->_onRefresh) {
+ self->_onRefresh(nil);
+ }
+ }
+ ];
+}
+
@end
diff --git a/React/Views/RefreshControl/RCTRefreshControlManager.m b/React/Views/RefreshControl/RCTRefreshControlManager.m
@@ -224,6 +175,35 @@ index 8b6571698fc5dd091a0d8980a33bb40295faf305..27c97bfeb6f13907c89f1d85f2bb8b8a
reactChoreographer.postFrameCallback(ReactChoreographer.CallbackType.IDLE_EVENT, this)
}
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
index 89b666dcf0258df0702c812600b685463128294c..2b1c3971f0c31a0d7a592b90170e4cc53a8a69dd 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
@@ -431,6 +431,13 @@ public open class ReactViewGroup public constructor(context: Context?) :
inSubviewClippingLoop = true
var clippedSoFar = 0
for (i in 0..<allChildrenCount) {
+ // Reentrant child removal during this loop can compact allChildren and leave a null at
+ // an index below allChildrenCount. A null entry means the view is already detached, so
+ // treat it as clipped instead of crashing.
+ if (childArray[i] == null) {
+ clippedSoFar++
+ continue
+ }
try {
updateSubviewClipStatus(clippingRect, i, clippedSoFar, excludedViewsSet)
} catch (ex: IndexOutOfBoundsException) {
@@ -466,7 +473,9 @@ public open class ReactViewGroup public constructor(context: Context?) :
) {
assertOnUiThread()
- val child = checkNotNull(allChildren?.get(idx))
+ // allChildren can be mutated reentrantly while a clipping pass is running, so a stale
+ // index can point at a null slot. Skip it instead of crashing.
+ val child = allChildren?.get(idx) ?: return
val intersects = clippingRect.intersects(child.left, child.top, child.right, child.bottom)
var needUpdateClippingRecursive = false
diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
index 216bb23beb023ef6c3ae814c17e05bccbda7fc91..6ad5cc1d9ed5b8cd2df08ad77adca56c6bb58ff4 100644
--- a/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm
+27 -6
View File
@@ -6,12 +6,6 @@ Patching `RCTRefreshControl.mm` temporarily to play an impact haptic on refresh
17.4, there has been a regression somewhere causing haptics to not play on iOS on refresh. Should monitor for an update
in the RN repo: https://github.com/facebook/react-native/issues/43388
## RefreshControl Path - ScrollForwarder
Patching `RCTRefreshControl.m` and `RCTRefreshControl.h` to add a new `forwarderBeginRefreshing` method to the class.
This method is used by `ExpoScrollForwarder` to initiate a refresh of the underlying `UIScrollView` from inside that
module.
## RCTPullToRefreshViewComponentView.mm Patch - RefreshControl initial props dropped on New Arch
**TODO: Remove after bumping React Native to 0.82+** (fixed upstream by facebook/react-native#52615, #52584
@@ -65,6 +59,33 @@ content-less area are attributed to the `UIScrollView`.
Issue: https://github.com/facebook/react-native/issues/54123
PR: https://github.com/react/react-native/pull/56747
## ReactViewGroup.kt Patch - Fatal "Required value was null" during subview clipping on Android
Fixes Sentry issue APP-T20Q: `IllegalStateException: Required value was null` thrown by
`checkNotNull(allChildren?.get(idx))` in `updateSubviewClipStatus`, reached from
`ReactScrollView.onScrollChanged -> updateClippingRect` during an animated smooth scroll
(New Architecture, `removeClippedSubviews`).
The clipping loop in `updateClippingToRect` captures its bound once, but clipping a view
(`removeViewsInLayout`) can synchronously trigger reentrant child removal (layout-change
listeners, animation-end callbacks, Fabric mounting on the UI thread), which compacts
`allChildren` and nulls the tail mid-loop. Upstream already catches the
`IndexOutOfBoundsException` variant of this corruption with diagnostics, but the null-child
variant throws `IllegalStateException` and escapes as a fatal crash. A null entry means the
view is already detached, so we skip it and count it as clipped to keep index math aligned.
Not fixed upstream as of July 2026 (identical `checkNotNull` on `main`); the sibling fix
attempt facebook/react-native#57365 for the same bookkeeping corruption (different stack)
was abandoned. Re-check when bumping React Native.
Note on build modes: production Android builds compile react-android from source
(`buildReactNativeFromSource: IS_PRODUCTION` via expo-build-properties in app.config.js
injects the includeBuild/dependency-substitution block at prebuild), so this hunk IS
active in production releases. Local dev builds prebuilt in a non-production env consume
the prebuilt AAR from Maven Central instead, where this hunk (like any ReactAndroid
source change) has no effect - do not expect to see the fix in a local debug build unless
you prebuild with EXPO_PUBLIC_ENV=production or add the substitution block manually.
## RCTTextLayoutManager.mm Patch - Text overflows instead of wrapping on the last line
Issue: https://github.com/react/react-native/issues/53450#issuecomment-3298157830
-39
View File
@@ -1,39 +0,0 @@
diff --git a/lib/module/toast.js b/lib/module/toast.js
index be089f3ff23017a6844e2010cedc547729e198aa..c2dd0fa2df93f929c33bf2bcc3e6f921941fa006 100644
--- a/lib/module/toast.js
+++ b/lib/module/toast.js
@@ -1,8 +1,8 @@
"use strict";
import * as React from 'react';
-import { ActivityIndicator, Pressable, Text, View } from 'react-native';
-import Animated, { useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
+import { ActivityIndicator, Pressable, Text, View, Platform } from 'react-native';
+import Animated, { useAnimatedStyle, useSharedValue, withRepeat, withTiming, FadeOut } from 'react-native-reanimated';
import { ANIMATION_DURATION, useToastLayoutAnimations } from "./animations.js";
import { toastDefaultValues } from "./constants.js";
import { useToastContext } from "./context.js";
@@ -258,7 +258,10 @@ export const Toast = /*#__PURE__*/React.forwardRef(({
...toastSwipeHandlerProps,
children: /*#__PURE__*/_jsx(Animated.View, {
entering: entering,
- exiting: exiting,
+ exiting: Platform.select({
+ android: undefined,
+ default: exiting
+ }),
children: jsx
})
});
@@ -268,7 +271,10 @@ export const Toast = /*#__PURE__*/React.forwardRef(({
children: /*#__PURE__*/_jsx(Animated.View, {
style: [unstyled ? undefined : elevationStyle, defaultStyles.toast, toastStyleCtx, styles?.toast, style, wiggleAnimationStyle],
entering: entering,
- exiting: exiting,
+ exiting: Platform.select({
+ android: undefined,
+ default: exiting
+ }),
children: /*#__PURE__*/_jsxs(View, {
style: [defaultStyles.toastContent, toastContentStyleCtx, styles?.toastContent],
children: [promiseOptions || variant === 'loading' ? 'loading' in icons ? icons.loading : /*#__PURE__*/_jsx(ActivityIndicator, {}) : icon ? /*#__PURE__*/_jsx(View, {
-3
View File
@@ -1,3 +0,0 @@
# sonner-native+0.21.0.patch
Removes Reanimated exit layout animations from the toasts. This was causing crashes if the toast was hidden while you were scrolling a flatlist.
+391 -390
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -39,7 +39,6 @@ patchedDependencies:
'react-native-svg@15.12.1': patches/react-native-svg@15.12.1.patch
'react-native-view-shot@4.0.3': patches/react-native-view-shot@4.0.3.patch
'react-native@0.81.5': patches/react-native@0.81.5.patch
'sonner-native@0.21.0': patches/sonner-native@0.21.0.patch
minimumReleaseAgeExclude:
- '@atproto/*'
- '@bsky.app/*'
+20 -2
View File
@@ -14,8 +14,26 @@ if [ -z "$RUNTIME_VERSION" ]; then
fi
cd bundleTempDir || exit
BUNDLE_VERSION=$(date +%s)
DEPLOYMENT_URL="https://updates.bsky.app/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION&channel=$CHANNEL_NAME&ios-build-number=$BSKY_IOS_BUILD_NUMBER&android-build-number=$BSKY_ANDROID_VERSION_CODE"
# Shared with denisPublish.sh when both run in one job -- see the note there.
# Both origins must receive the same bundle version for the same bytes, because
# the version is part of the asset URL path.
BUNDLE_VERSION="${BUNDLE_VERSION:-$(date +%s)}"
# This MUST address ota1's own origin hostname, never updates.bsky.app.
#
# Since the 2026-07-26 cutover updates.bsky.app resolves to denis on EKS, which
# deliberately has no /v1/upload route -- publishing there is out-of-band via
# `denis publish` (see denisPublish.sh). Posting to the CDN hostname therefore
# returns 404, which is what broke this step the first time it ran after the
# flip. The dual-write was never independent of the cutover precisely because it
# addressed the hostname being cut over.
#
# This upload exists only to keep ota1 carrying current bundles so a rollback of
# the Bunny origin remains useful. It goes away with this whole script when ota1
# is decommissioned (Phase 5).
OTA1_ORIGIN="${OTA1_ORIGIN:-https://ota1.us-east.updates.bsky.network}"
DEPLOYMENT_URL="$OTA1_ORIGIN/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION&channel=$CHANNEL_NAME&ios-build-number=$BSKY_IOS_BUILD_NUMBER&android-build-number=$BSKY_ANDROID_VERSION_CODE"
tar czvf bundle.tar.gz ./*
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
# Publishes the just-exported Expo bundle to the denis OTA service (S3) via the
# `denis publish` CLI. Mirrors bundleUpdate.sh's inputs (runtime version, bundle
# version, build numbers) but targets denis instead of the legacy ota1 upload.
# Expects: the `denis` binary on PATH (setup-denis action), ambient AWS creds
# (configure-aws-credentials OIDC), and BSKY_IOS_BUILD_NUMBER /
# BSKY_ANDROID_VERSION_CODE from the use-build-number wrapper.
rm -rf bundleTempDir
echo "Assembling bundle directory..."
node scripts/bundleUpdate.js
if [ -z "$RUNTIME_VERSION" ]; then
RUNTIME_VERSION=$(cat package.json | jq '.version' -r)
fi
# Accept a caller-supplied bundle version so that a dual-write publishes the SAME
# version to every origin. When this script and bundleUpdate.sh each called
# `date +%s` independently they produced versions seconds apart for identical
# bytes -- observed 1785102575 (denis) vs 1785102614 (ota1) for one commit. Since
# the version is part of the asset URL path, the two origins then served
# manifests pointing at paths only one of them had, so the manifest and its
# assets had to come from the same origin or the fetch 404s. Falling back to
# `date +%s` keeps standalone callers (PR previews, `pnpm make-deploy-bundle`)
# working unchanged.
BUNDLE_VERSION="${BUNDLE_VERSION:-$(date +%s)}"
DENIS_CDN_DOMAIN="${DENIS_CDN_DOMAIN:-updates.bsky.app}"
DENIS_S3_BUCKET="${DENIS_S3_BUCKET:-bsky-denis-ota-prod}"
echo "Publishing to denis..."
echo " runtime-version: $RUNTIME_VERSION"
echo " bundle-version: $BUNDLE_VERSION"
echo " channel: $CHANNEL_NAME"
echo " ios-build-number: $BSKY_IOS_BUILD_NUMBER"
echo " android-build-number: $BSKY_ANDROID_VERSION_CODE"
echo " cdn-domain: $DENIS_CDN_DOMAIN"
echo " s3-bucket: $DENIS_S3_BUCKET"
denis publish \
--bundle-dir bundleTempDir \
--runtime-version "$RUNTIME_VERSION" \
--bundle-version "$BUNDLE_VERSION" \
--channel "$CHANNEL_NAME" \
--ios-build-number "$BSKY_IOS_BUILD_NUMBER" \
--android-build-number "$BSKY_ANDROID_VERSION_CODE" \
--cdn-domain "$DENIS_CDN_DOMAIN" \
--s3-bucket "$DENIS_S3_BUCKET"
rm -rf bundleTempDir
+2 -2
View File
@@ -9,13 +9,13 @@ import {
import Animated, {
Easing,
interpolate,
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Svg, {Path, type SvgProps} from 'react-native-svg'
import {scheduleOnRN} from 'react-native-worklets'
import {Image} from 'expo-image'
import * as SplashScreen from 'expo-splash-screen'
@@ -136,7 +136,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
1,
{duration: 1200, easing: Easing.in(Easing.cubic)},
() => {
runOnJS(onFinish)()
scheduleOnRN(onFinish)
},
),
)
+18 -1
View File
@@ -4,13 +4,30 @@ import {
} from '@atproto/api'
import {AgeAssuranceAccess} from '#/ageAssurance/types'
import {ANDROID_API_LEVEL, IOS_MAJOR_VERSION, IS_ANDROID, IS_IOS} from '#/env'
import {
ANDROID_API_LEVEL,
IOS_MAJOR_VERSION,
IS_ANDROID,
IS_IOS,
IS_WEB,
} from '#/env'
/**
* Minimum age required to access the app at all.
*/
export const MIN_ACCESS_AGE = 13
/**
* The identifier for the current platform, matching the `knownValues` of the
* `platforms` property on `app.bsky.ageassurance.defs#configRegion`. Used to
* filter out region configs that don't apply to this platform.
*/
export const AGE_ASSURANCE_PLATFORM: 'web' | 'ios' | 'android' = IS_WEB
? 'web'
: IS_IOS
? 'ios'
: 'android'
/**
* Whether the current device can provide the native on-device age signals we
* use for age assurance (via `expo-age-range`). We gate on OS version because
+3 -2
View File
@@ -59,9 +59,10 @@ export const config: AppBskyAgeassuranceDefs.Config = {
],
},
{
// On-device verification region. KWS is included as a fallback for
// platforms without the native age API (e.g. web) or when the device
// On-device verification region, native-only (web users in TX are not
// age assured). KWS is included as a fallback for when the device
// result is insufficient.
platforms: ['ios', 'android'],
countryCode: 'US',
regionCode: 'TX',
minAccessAge: 18,
+31
View File
@@ -0,0 +1,31 @@
import {getAgeAssuranceRegionConfig} from '@atproto/api'
import {getAgeAssuranceRegionConfigForGeolocation} from '#/ageAssurance/util'
jest.mock('#/ageAssurance/data')
jest.mock('@atproto/api', () => ({
...jest.requireActual('@atproto/api'),
getAgeAssuranceRegionConfig: jest.fn(),
}))
/*
* Platform-based region filtering itself is implemented and tested in
* `@atproto/api` (see `getAgeAssuranceRegionConfig`). What we own - and test
* here - is that region resolution passes the current platform through. The
* jest preset is `jest-expo/ios`, so `AGE_ASSURANCE_PLATFORM` resolves to
* `ios` in these tests.
*/
describe('getAgeAssuranceRegionConfigForGeolocation', () => {
it('passes the current platform to the SDK region matcher', () => {
const config = {regions: []}
getAgeAssuranceRegionConfigForGeolocation(config, {
countryCode: 'US',
regionCode: 'TX',
})
expect(getAgeAssuranceRegionConfig).toHaveBeenCalledWith(config, {
countryCode: 'US',
regionCode: 'TX',
platform: 'ios',
})
})
})
+6
View File
@@ -11,6 +11,7 @@ import {getAge} from '#/lib/strings/time'
import {regionName} from '#/locale/helpers'
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/const'
import {
AGE_ASSURANCE_PLATFORM,
DEVICE_SIGNALS_SUPPORTED,
FALLBACK_REGION_CONFIG,
MIN_ACCESS_AGE,
@@ -29,6 +30,10 @@ import {USRegionNameToRegionCode} from '#/geolocation/util'
* Resolves a geolocation to its matched age assurance region config, or
* undefined when the geolocation matches no AA region.
*
* Regions scoped to other platforms via `platforms` are passed over entirely,
* as if they weren't in the config - a later region matching the same
* geolocation can still apply.
*
* This is the single source of truth for geolocation -> region resolution.
* Device signals are written and read back under a key derived from the
* matched region (see `createRegionKey`), so every site that resolves a region
@@ -42,6 +47,7 @@ export function getAgeAssuranceRegionConfigForGeolocation(
return getAgeAssuranceRegionConfig(config, {
countryCode: geolocation.countryCode ?? '',
regionCode: geolocation.regionCode,
platform: AGE_ASSURANCE_PLATFORM,
})
}
+1 -1
View File
@@ -4,7 +4,7 @@ import {
type TextProps as RNTextProps,
type TextStyle,
} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import {UITextView} from '@bsky.app/react-native-uitextview'
import createEmojiRegex from 'emoji-regex'
import {type Alf, applyFonts, atoms, flatten} from '#/alf'
+4
View File
@@ -103,6 +103,10 @@ export type Events = {
}
'signup:captchaSuccess': {}
'signup:captchaFailure': {}
'signup:captchaBackPress': {}
'signup:createAccountFailure': {
reason: string
}
'signup:fieldError': {
field: string
errorCount: number
+5 -5
View File
@@ -30,7 +30,6 @@ import {KeyboardEvents} from 'react-native-keyboard-controller'
import Animated, {
clamp,
interpolate,
runOnJS,
type SharedValue,
useAnimatedReaction,
useAnimatedStyle,
@@ -44,6 +43,7 @@ import {
useSafeAreaInsets,
} from 'react-native-safe-area-context'
import {captureRef} from 'react-native-view-shot'
import {scheduleOnRN} from 'react-native-worklets'
import {Image, type ImageErrorEventData} from 'expo-image'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -166,7 +166,7 @@ export function Root({children}: {children: React.ReactNode}) {
// note: return location has to be reset on open,
// rather than on close, otherwise there's a flicker
// where the reanimated update is faster than the react render
runOnJS(onCompletedClose)()
scheduleOnRN(onCompletedClose)
}
}),
)
@@ -331,7 +331,7 @@ export function Trigger({
() => hoveredItemSV.get(),
(hovered, prev) => {
if (hovered !== prev) {
runOnJS(setHoveredMenuItem)(hovered)
scheduleOnRN(setHoveredMenuItem, hovered)
}
},
)
@@ -343,7 +343,7 @@ export function Trigger({
.averageTouches(true)
.onStart(() => {
'worklet'
runOnJS(open)('full')
scheduleOnRN(open, 'full')
})
.onUpdate(evt => {
'worklet'
@@ -357,7 +357,7 @@ export function Trigger({
// as the menu may have slid into place beneath their finger
const item = hoveredItemSV.get()
if (item) {
runOnJS(onTouchUpMenuItem)(item)
scheduleOnRN(onTouchUpMenuItem, item)
}
})
}, [open, hoverablesSV, onTouchUpMenuItem, hoveredItemSV, translationSV])
+5 -21
View File
@@ -21,11 +21,11 @@ import {
} from 'react-native'
import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller'
import Animated, {
runOnJS,
type ScrollEvent,
useAnimatedStyle,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -197,24 +197,8 @@ export function Outer({
/**
* @deprecated use `Dialog.ScrollableInner` instead
*/
export function Inner({children, style, header}: DialogInnerProps) {
const insets = useSafeAreaInsets()
return (
<>
{header}
<View
style={[
a.pt_2xl,
a.px_xl,
IS_LIQUID_GLASS
? a.pb_2xl
: {paddingBottom: insets.bottom + insets.top},
style,
]}>
{children}
</View>
</>
)
export function Inner(props: DialogInnerProps) {
return <ScrollableInner {...props} />
}
export const ScrollableInner = forwardRef<ScrollView, DialogInnerProps>(
@@ -315,9 +299,9 @@ export const InnerFlatList = forwardRef<
}
const {contentOffset} = e
if (contentOffset.y > 0 && !disableDrag) {
runOnJS(setDisableDrag)(true)
scheduleOnRN(setDisableDrag, true)
} else if (contentOffset.y <= 1 && disableDrag) {
runOnJS(setDisableDrag)(false)
scheduleOnRN(setDisableDrag, false)
}
}
+3
View File
@@ -165,6 +165,9 @@ export function Outer({
)
}
/**
* @deprecated use `Dialog.ScrollableInner` instead
*/
export function Inner({
children,
style,
+9 -9
View File
@@ -4,7 +4,6 @@ import Animated, {
type AnimatedRef,
measure,
Reanimated3DefaultSpringConfig,
runOnJS,
scrollTo,
type SharedValue,
useAnimatedRef,
@@ -14,6 +13,7 @@ import Animated, {
withSpring,
withTiming,
} from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {useHaptics} from '#/lib/haptics'
import {atoms as a, useTheme, web} from '#/alf'
@@ -26,7 +26,7 @@ import {IS_IOS} from '#/env'
*
* All positioning is driven by a `slots` map (key → index) and translateY
* (no discrete `top` changes). On drag end the new slot assignment is
* computed on the UI thread first, then React state is updated via runOnJS.
* computed on the UI thread first, then React state is updated via scheduleOnRN.
*
* See SortableList.web.tsx for the web implementation using pointer events.
*/
@@ -42,7 +42,7 @@ interface SortableListProps<T> {
itemHeight: number
/** Ref to the parent Animated.ScrollView for auto-scroll. */
scrollRef?: AnimatedRef<Animated.ScrollView>
/** Scroll offset shared value from useScrollViewOffset. */
/** Scroll offset shared value from useScrollOffset. */
scrollOffset?: SharedValue<number>
}
@@ -265,9 +265,9 @@ function SortableItem<T>({
measureDone.set(false)
lastHapticSlot.set(mySlot)
if (onDragStart) {
runOnJS(onDragStart)()
scheduleOnRN(onDragStart)
}
runOnJS(playHaptic)()
scheduleOnRN(playHaptic)
})
.onChange(e => {
'worklet'
@@ -285,7 +285,7 @@ function SortableItem<T>({
const clampedSlot = Math.max(0, Math.min(currentSlot, itemCount - 1))
if (IS_IOS && clampedSlot !== lastHapticSlot.get()) {
lastHapticSlot.set(clampedSlot)
runOnJS(playHaptic)('Light')
scheduleOnRN(playHaptic, 'Light')
}
})
.onEnd(() => {
@@ -326,13 +326,13 @@ function SortableItem<T>({
dragStartSlot: -1,
})
dragY.set(0)
runOnJS(onCommitReorder)(sorted)
scheduleOnRN(onCommitReorder, sorted)
} else {
const s = state.get()
state.set({...s, activeKey: '', dragStartSlot: -1})
dragY.set(0)
if (onDragEnd) {
runOnJS(onDragEnd)()
scheduleOnRN(onDragEnd)
}
}
}
@@ -347,7 +347,7 @@ function SortableItem<T>({
const s = state.get()
state.set({...s, activeKey: '', dragStartSlot: -1})
if (onDragEnd) {
runOnJS(onDragEnd)()
scheduleOnRN(onDragEnd)
}
}
})
+1 -1
View File
@@ -22,7 +22,7 @@ interface SortableListProps<T> {
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. */
/** Scroll offset shared value from useScrollOffset. Ignored on web. */
scrollOffset?: SharedValue<number>
}
+2 -2
View File
@@ -2,12 +2,12 @@ import {useRef, useState} from 'react'
import {Modal, Pressable, StyleSheet, View} from 'react-native'
import Animated, {
interpolate,
runOnJS,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming,
} from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {useLingui} from '@lingui/react/macro'
import {atoms as a} from '#/alf'
@@ -52,7 +52,7 @@ export function ImageMenu({onPressShare, onPressSave}: Props) {
progress.set(
withTiming(0, TIMING_OUT, finished => {
if (finished) {
runOnJS(setIsMounted)(false)
scheduleOnRN(setIsMounted, false)
}
}),
)
@@ -8,7 +8,6 @@ import {
import Animated, {
type AnimatableValue,
Reanimated3DefaultSpringConfig,
runOnJS,
type SharedValue,
useAnimatedReaction,
useAnimatedRef,
@@ -16,6 +15,7 @@ import Animated, {
useSharedValue,
withSpring,
} from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {Image} from 'expo-image'
import {
@@ -95,7 +95,7 @@ const ImageItem = ({
},
(nextIsScaled, prevIsScaled) => {
if (nextIsScaled !== prevIsScaled) {
runOnJS(handleZoom)(nextIsScaled)
scheduleOnRN(handleZoom, nextIsScaled)
}
},
)
@@ -245,7 +245,7 @@ const ImageItem = ({
const singleTap = Gesture.Tap().onEnd(() => {
'worklet'
runOnJS(onTap)()
scheduleOnRN(onTap)
})
const doubleTap = Gesture.Tap()
@@ -359,9 +359,9 @@ const ImageItem = ({
},
(show, prevShow) => {
if (!prevShow && show) {
runOnJS(setShowLoader)(true)
scheduleOnRN(setShowLoader, true)
} else if (prevShow && !show) {
runOnJS(setShowLoader)(false)
scheduleOnRN(setShowLoader, false)
}
},
)
@@ -14,7 +14,6 @@ import {
type PanGesture,
} from 'react-native-gesture-handler'
import Animated, {
runOnJS,
type SharedValue,
useAnimatedProps,
useAnimatedReaction,
@@ -24,6 +23,7 @@ import Animated, {
useSharedValue,
} from 'react-native-reanimated'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import {Image} from 'expo-image'
import {
@@ -84,7 +84,7 @@ const ImageItem = ({
'worklet'
const nextIsScaled = e.zoomScale > 1
if (scaled !== nextIsScaled) {
runOnJS(handleZoom)(nextIsScaled)
scheduleOnRN(handleZoom, nextIsScaled)
}
},
onBeginDrag() {
@@ -118,7 +118,7 @@ const ImageItem = ({
const singleTap = Gesture.Tap().onEnd(() => {
'worklet'
runOnJS(onTap)()
scheduleOnRN(onTap)
})
const doubleTap = Gesture.Tap()
@@ -142,7 +142,7 @@ const ImageItem = ({
screenSize,
)
}
runOnJS(zoomTo)(nextZoomRect)
scheduleOnRN(zoomTo, nextZoomRect)
})
const composedGesture = Gesture.Exclusive(
@@ -199,9 +199,9 @@ const ImageItem = ({
},
(show, prevShow) => {
if (!prevShow && show) {
runOnJS(setShowLoader)(true)
scheduleOnRN(setShowLoader, true)
} else if (prevShow && !show) {
runOnJS(setShowLoader)(false)
scheduleOnRN(setShowLoader, false)
}
},
)
+10 -11
View File
@@ -20,8 +20,6 @@ import Animated, {
measure,
type MeasuredDimensions,
ReduceMotion,
runOnJS,
runOnUI,
type SharedValue,
useAnimatedReaction,
useAnimatedRef,
@@ -32,6 +30,7 @@ import Animated, {
withSpring,
type WithSpringConfig,
} from 'react-native-reanimated'
import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets'
import {Image} from 'expo-image'
import * as ScreenOrientation from 'expo-screen-orientation'
@@ -136,10 +135,10 @@ export default function ImageViewRoot({
const onFullyClosed = useCallback(() => {
setActiveLightbox(null)
runOnUI(() => {
scheduleOnUI(() => {
'worklet'
thumbRects.set({})
})()
})
requestIdleCallback(() => {
void Image.clearMemoryCache()
})
@@ -149,7 +148,7 @@ export default function ImageViewRoot({
() => openProgress.get() === 0,
(isGone, wasGone) => {
if (isGone && !wasGone) {
runOnJS(onFullyClosed)()
scheduleOnRN(onFullyClosed)
}
},
)
@@ -160,10 +159,10 @@ export default function ImageViewRoot({
() => openProgress.get() === 1,
(isOpen, wasOpen) => {
if (isOpen && !wasOpen) {
runOnJS(ScreenOrientation.unlockAsync)()
scheduleOnRN(ScreenOrientation.unlockAsync)
} else if (!isOpen && wasOpen) {
// default is PORTRAIT_UP - set via config plugin in app.config.js -sfn
runOnJS(ScreenOrientation.lockAsync)(PORTRAIT_UP)
scheduleOnRN(ScreenOrientation.lockAsync, PORTRAIT_UP)
}
},
)
@@ -171,7 +170,7 @@ export default function ImageViewRoot({
const onFlyAway = useCallback(() => {
'worklet'
openProgress.set(0)
runOnJS(onRequestClose)()
scheduleOnRN(onRequestClose)
}, [onRequestClose, openProgress])
return (
@@ -320,7 +319,7 @@ function ImageView({
const handleRequestClose = useCallback(() => {
const activeRef = images[imageIndex]?.thumbRef
if (isAnimated && activeRef) {
runOnUI(() => {
scheduleOnUI(() => {
'worklet'
const rect = measure(activeRef)
thumbRects.modify(rects => {
@@ -328,8 +327,8 @@ function ImageView({
rects[imageIndex] = rect
return rects
})
runOnJS(onRequestClose)()
})()
scheduleOnRN(onRequestClose)
})
} else {
onRequestClose()
}
+6 -10
View File
@@ -1,10 +1,6 @@
import {createContext, useContext, useEffect, useMemo, useState} from 'react'
import {
measure,
type MeasuredDimensions,
runOnJS,
runOnUI,
} from 'react-native-reanimated'
import {measure, type MeasuredDimensions} from 'react-native-reanimated'
import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets'
import {nanoid} from 'nanoid/non-secure'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -73,7 +69,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
if (thumbRef) {
// Measure the tapped image on the UI thread, then open with
// the rect baked in so it's available from the first render.
// Only the rect (plain data) goes through runOnJS — AnimatedRef
// Only the rect (plain data) goes through scheduleOnRN — AnimatedRef
// objects can't survive serialization across threads.
const openWithRect = (rect: MeasuredDimensions | null) => {
doOpen({
@@ -83,11 +79,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
),
})
}
runOnUI(() => {
scheduleOnUI(() => {
'worklet'
const rect = measure(thumbRef)
runOnJS(openWithRect)(rect)
})()
scheduleOnRN(openWithRect, rect)
})
} else {
doOpen(lightbox)
}
@@ -8,12 +8,12 @@ import {
} from 'react-native'
import Animated, {
measure,
runOnJS,
useAnimatedRef,
useFrameCallback,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {WebView} from 'react-native-webview'
import {scheduleOnRN} from 'react-native-worklets'
import {Image} from 'expo-image'
import {type AppBskyEmbedExternal} from '@atproto/api'
import {msg} from '@lingui/core/macro'
@@ -164,7 +164,7 @@ export function ExternalPlayer({
const isVisible = top <= realWinHeight - insets.bottom && bot >= insets.top
if (!isVisible) {
runOnJS(setIsPlayerActive)(false)
scheduleOnRN(setIsPlayerActive, false)
}
}, false) // False here disables autostarting the callback
+3 -3
View File
@@ -9,12 +9,12 @@ import {
import {Pressable, useWindowDimensions, View} from 'react-native'
import Animated, {
Easing,
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -69,7 +69,7 @@ export const ProgressGuideToast = forwardRef<
duration: 400,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(setIsntOpen)(),
() => scheduleOnRN(setIsntOpen),
),
)
}, [setIsOpen, opacity])
@@ -88,7 +88,7 @@ export const ProgressGuideToast = forwardRef<
duration: 100,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(playCheckmark)(),
() => scheduleOnRN(playCheckmark),
),
)
translateY.set(0)
+4 -3
View File
@@ -425,9 +425,10 @@ function Bubble({
},
]}
onLayout={e => {
setBubbleMeasurements({
width: e.nativeEvent.layout.width,
height: e.nativeEvent.layout.height,
const {width, height} = e.nativeEvent.layout
setBubbleMeasurements(prev => {
if (prev?.width === width && prev.height === height) return prev
return {width, height}
})
}}>
{children}
+1 -1
View File
@@ -1,4 +1,4 @@
import {UITextView} from 'react-native-uitextview'
import {UITextView} from '@bsky.app/react-native-uitextview'
import {logger} from '#/logger'
import {atoms as a, type TextStyleProp, useAlf, useTheme, web} from '#/alf'
+2 -2
View File
@@ -1,12 +1,12 @@
import {useCallback} from 'react'
import {Pressable, View} from 'react-native'
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
import {
ScaleAndFadeIn,
@@ -41,7 +41,7 @@ export function NewMessagesPill({
}, [scale])
const onPress = useCallback(() => {
runOnJS(playHaptic)()
scheduleOnRN(playHaptic)
onPressInner?.()
}, [onPressInner, playHaptic])
+3 -3
View File
@@ -4,13 +4,13 @@ import {Gesture, type GestureType} from 'react-native-gesture-handler'
import Animated, {
clamp,
interpolate,
runOnJS,
useAnimatedStyle,
useReducedMotion,
useSharedValue,
withSequence,
withTiming,
} from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {useHaptics} from '#/lib/haptics'
import {atoms as a, tokens, useTheme} from '#/alf'
@@ -111,7 +111,7 @@ export function SwipeToReply({
if (pastThreshold && !hit.get()) {
hit.set(true)
runPop()
runOnJS(playHaptic)('Medium')
scheduleOnRN(playHaptic, 'Medium')
} else if (!pastThreshold && hit.get()) {
hit.set(false)
}
@@ -120,7 +120,7 @@ export function SwipeToReply({
'worklet'
// Only a clean end (finger lifted past threshold) triggers the reply.
if (hit.get()) {
runOnJS(onReply)()
scheduleOnRN(onReply)
}
})
.onFinalize(() => {
+2
View File
@@ -95,6 +95,8 @@ export function Root({children, isInvalid = false, style}: RootProps) {
a.relative,
a.w_full,
a.px_md,
// Contain the input's z-index so it cannot paint over nearby overlays.
{zIndex: 0},
style,
]}
{...web({
@@ -1,9 +1,7 @@
import {useMemo} from 'react'
import {Pressable, View} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
import {type AppBskyUnspeccedDefs, moderateProfile} from '@atproto/api'
import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useTrendingSettings} from '#/state/preferences/trending'
@@ -51,16 +49,6 @@ function Inner() {
const shadowColor = alpha(t.palette.primary_100, 0.5)
const gradient = {
values: [
[0, t.atoms.bg.backgroundColor],
[0.1, t.palette.primary_25],
[0.9, t.palette.primary_25],
[1, t.atoms.bg.backgroundColor],
],
hover_value: t.palette.white,
}
if (error || noTopics) {
return null
}
@@ -75,13 +63,6 @@ function Inner() {
a.border_t,
t.atoms.border_contrast_low,
]}>
<LinearGradient
colors={gradient.values.map(c => c[1]) as [string, string, ...string[]]}
locations={
gradient.values.map(c => c[0]) as [number, number, ...number[]]
}
style={[a.absolute, a.inset_0]}
/>
<View
style={[
a.relative,
@@ -94,23 +75,27 @@ function Inner() {
]}>
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_xs]}>
<TrendingIcon width={18} />
<Text
style={[a.text_md, a.font_medium, a.leading_snug]}
numberOfLines={1}>
<Text style={[a.text_md, a.font_medium]} numberOfLines={1}>
<Trans>Trending</Trans>
</Text>
</View>
<Link label={l`See more trending topics`} to="/search">
<Text
style={[
a.text_sm,
a.font_medium,
a.leading_snug,
t.atoms.text_contrast_high,
]}
numberOfLines={1}>
<Trans>See more</Trans>
</Text>
{({hovered, pressed}) => (
<Text
style={[
a.text_sm,
a.font_medium,
{
color:
hovered || pressed
? t.palette.contrast_800
: t.palette.contrast_500,
},
]}
numberOfLines={1}>
<Trans>See more</Trans>
</Text>
)}
</Link>
</View>
<View
@@ -163,6 +148,7 @@ function TrendRow({
const {t: l, i18n} = useLingui()
const actors = useModerateTrendingActors(trend.actors)
const formattedPostCount = formatCount(i18n, trend.postCount)
return (
<Link
@@ -215,14 +201,14 @@ function TrendRow({
<Text
style={[a.text_sm, t.atoms.text_contrast_medium]}
numberOfLines={1}>
{trend.postCount >= 1000 ? (
<Trans comment="Over 1,000 posts">1K+ posts</Trans>
) : (
<Trans comment="'{postCount} {posts}', e.g., '1.2K posts'">
{formatCount(i18n, trend.postCount)}{' '}
{plural(trend.postCount, {one: 'post', other: 'posts'})}
</Trans>
)}
<Trans comment="'{postCount} {posts}', e.g., '1.2K posts'">
{formattedPostCount}{' '}
<Plural
value={{postCount: trend.postCount}}
one="post"
other="posts"
/>
</Trans>
</Text>
</View>
</View>
+14 -14
View File
@@ -5,7 +5,6 @@ import Animated, {
clamp,
interpolate,
interpolateColor,
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useDerivedValue,
@@ -14,6 +13,7 @@ import Animated, {
withSequence,
withTiming,
} from 'react-native-reanimated'
import {scheduleOnRN} from 'react-native-worklets'
import {useHaptics} from '#/lib/haptics'
import {type GestureActions} from './GestureActionView.shared'
@@ -74,17 +74,17 @@ export function GestureActionView({
() => transX,
() => {
if (transX.get() === 0) {
runOnJS(setActiveAction)(null)
scheduleOnRN(setActiveAction, null)
} else if (transX.get() < 0) {
if (
actions.leftSecond &&
transX.get() <= -actions.leftSecond.threshold
) {
if (activeAction !== 'leftSecond') {
runOnJS(setActiveAction)('leftSecond')
scheduleOnRN(setActiveAction, 'leftSecond')
}
} else if (activeAction !== 'leftFirst') {
runOnJS(setActiveAction)('leftFirst')
scheduleOnRN(setActiveAction, 'leftFirst')
}
} else if (transX.get() > 0) {
if (
@@ -92,10 +92,10 @@ export function GestureActionView({
transX.get() > actions.rightSecond.threshold
) {
if (activeAction !== 'rightSecond') {
runOnJS(setActiveAction)('rightSecond')
scheduleOnRN(setActiveAction, 'rightSecond')
}
} else if (activeAction !== 'rightFirst') {
runOnJS(setActiveAction)('rightFirst')
scheduleOnRN(setActiveAction, 'rightFirst')
}
}
},
@@ -127,7 +127,7 @@ export function GestureActionView({
!hitSecond.get()
) {
runPopAnimation()
runOnJS(haptic)()
scheduleOnRN(haptic)
hitSecond.set(true)
} else if (
hitSecond.get() &&
@@ -144,7 +144,7 @@ export function GestureActionView({
!hitFirst.get()
) {
runPopAnimation()
runOnJS(haptic)()
scheduleOnRN(haptic)
hitFirst.set(true)
} else if (
hitFirst.get() &&
@@ -161,7 +161,7 @@ export function GestureActionView({
!hitSecond.get()
) {
runPopAnimation()
runOnJS(haptic)()
scheduleOnRN(haptic)
hitSecond.set(true)
} else if (
hitSecond.get() &&
@@ -178,7 +178,7 @@ export function GestureActionView({
!hitFirst.get()
) {
runPopAnimation()
runOnJS(haptic)()
scheduleOnRN(haptic)
hitFirst.set(true)
} else if (
hitFirst.get() &&
@@ -193,15 +193,15 @@ export function GestureActionView({
'worklet'
if (e.translationX < 0) {
if (hitSecond.get() && actions.leftSecond) {
runOnJS(actions.leftSecond.action)()
scheduleOnRN(actions.leftSecond.action)
} else if (hitFirst.get() && actions.leftFirst) {
runOnJS(actions.leftFirst.action)()
scheduleOnRN(actions.leftFirst.action)
}
} else if (e.translationX > 0) {
if (hitSecond.get() && actions.rightSecond) {
runOnJS(actions.rightSecond.action)()
scheduleOnRN(actions.rightSecond.action)
} else if (hitSecond.get() && actions.rightFirst) {
runOnJS(actions.rightFirst.action)()
scheduleOnRN(actions.rightFirst.action)
}
}
transX.set(() => withTiming(0, {duration: 200}))
+1 -8
View File
@@ -12,7 +12,7 @@ import {
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {IS_ANDROID, IS_IOS, IS_TESTFLIGHT} from '#/env'
import {IS_IOS, IS_TESTFLIGHT} from '#/env'
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
@@ -192,13 +192,6 @@ export function useOTAUpdates() {
return
}
// TEMP: disable wake-from-background OTA loading on Android.
// This is causing a crash when the thread view is open due to
// `maintainVisibleContentPosition`. See repro repo for more details:
// https://github.com/mozzius/ota-crash-repro
// Old Arch only - re-enable once we're on the New Archictecture! -sfn
if (IS_ANDROID) return
const subscription = AppState.addEventListener(
'change',
async nextAppState => {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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