Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9143920f0 | |||
| 0e63434bb4 | |||
| eb8f5fc4bc | |||
| 2ad84e540f | |||
| 97c2c516bd | |||
| 34827ef644 | |||
| 691619f297 | |||
| c2e5a9093c | |||
| 9b89a9adba | |||
| 22d657986d | |||
| abde10b7bd | |||
| 51bcacb20b | |||
| b662cd43b5 | |||
| 66ec855efc | |||
| d9e76c3954 | |||
| be70a31f4c | |||
| 2b9deeeb6a | |||
| d5e335e575 | |||
| 76a928a7af | |||
| 9eb78c0f6c | |||
| fb8bda861b | |||
| afd267bd2b | |||
| 93cb42b9f2 | |||
| 549e3d7a7d | |||
| 74a89832a3 | |||
| 19b0d89313 | |||
| 98e135f489 | |||
| fbb0ee38c2 | |||
| 7bd27e4180 | |||
| 07a810945d | |||
| 63b284030a | |||
| 3b6fbf315b | |||
| 97169dbc35 | |||
| 22a4958321 | |||
| a9df815bd0 | |||
| a53df16e82 | |||
| 4e3d1704a6 | |||
| 723a60e979 | |||
| 325246ec6c |
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) 🤖*
|
||||
@@ -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 }}`
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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`.'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -134,3 +134,6 @@ bskyweb/static/media/*.svg
|
||||
# superpowers plugin plans/specs — local-only workspace
|
||||
docs/superpowers/
|
||||
.claude/worktrees
|
||||
|
||||
# pnpm
|
||||
.pnpm-store/
|
||||
|
||||
+13
-11
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
@@ -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
@@ -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" |  |
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"env": {
|
||||
"EXPO_PUBLIC_ENV": "e2e",
|
||||
"NODE_ENV": "test",
|
||||
"NODE_ENV": "development",
|
||||
"RN_SRC_EXT": "e2e.ts,e2e.tsx"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
@@ -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.
|
||||
Generated
+391
-390
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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 ./*
|
||||
|
||||
|
||||
Executable
+54
@@ -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
@@ -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)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -103,6 +103,10 @@ export type Events = {
|
||||
}
|
||||
'signup:captchaSuccess': {}
|
||||
'signup:captchaFailure': {}
|
||||
'signup:captchaBackPress': {}
|
||||
'signup:createAccountFailure': {
|
||||
reason: string
|
||||
}
|
||||
'signup:fieldError': {
|
||||
field: string
|
||||
errorCount: number
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,9 @@ export function Outer({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `Dialog.ScrollableInner` instead
|
||||
*/
|
||||
export function Inner({
|
||||
children,
|
||||
style,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}))
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
+851
-829
File diff suppressed because it is too large
Load Diff
+862
-840
File diff suppressed because it is too large
Load Diff
+851
-829
File diff suppressed because it is too large
Load Diff
+863
-841
File diff suppressed because it is too large
Load Diff
+863
-841
File diff suppressed because it is too large
Load Diff
+863
-841
File diff suppressed because it is too large
Load Diff
+914
-892
File diff suppressed because it is too large
Load Diff
+866
-844
File diff suppressed because it is too large
Load Diff
+953
-931
File diff suppressed because it is too large
Load Diff
+851
-829
File diff suppressed because it is too large
Load Diff
+875
-853
File diff suppressed because it is too large
Load Diff
+851
-829
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
+270
-283
File diff suppressed because one or more lines are too long
+860
-838
File diff suppressed because it is too large
Load Diff
+851
-829
File diff suppressed because it is too large
Load Diff
+867
-845
File diff suppressed because it is too large
Load Diff
+851
-829
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+849
-827
File diff suppressed because it is too large
Load Diff
+851
-829
File diff suppressed because it is too large
Load Diff
+851
-829
File diff suppressed because it is too large
Load Diff
+851
-829
File diff suppressed because it is too large
Load Diff
+851
-829
File diff suppressed because it is too large
Load Diff
+863
-841
File diff suppressed because it is too large
Load Diff
+851
-829
File diff suppressed because it is too large
Load Diff
+890
-868
File diff suppressed because it is too large
Load Diff
+850
-828
File diff suppressed because it is too large
Load Diff
+851
-829
File diff suppressed because it is too large
Load Diff
+855
-833
File diff suppressed because it is too large
Load Diff
+850
-828
File diff suppressed because it is too large
Load Diff
+863
-841
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
Reference in New Issue
Block a user