From 21d53ce0f8f431706fca272e2108449203ce8f4b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 20 Jul 2026 17:07:30 +0300 Subject: [PATCH 01/32] Split mobile build/submit pipelines and de-duplicate OTA workflow (#11133) Co-authored-by: Claude Fable 5 --- .github/actions/compile-i18n/action.yml | 15 + .github/actions/setup-expo-project/action.yml | 47 +++ .github/actions/write-env/action.yml | 63 +++ .github/workflows/build-submit-android.yml | 184 +++++---- .github/workflows/build-submit-ios.yml | 187 +++++---- .../workflows/bundle-deploy-eas-update.yml | 372 +++--------------- 6 files changed, 400 insertions(+), 468 deletions(-) create mode 100644 .github/actions/compile-i18n/action.yml create mode 100644 .github/actions/setup-expo-project/action.yml create mode 100644 .github/actions/write-env/action.yml diff --git a/.github/actions/compile-i18n/action.yml b/.github/actions/compile-i18n/action.yml new file mode 100644 index 0000000000..9324ff17bf --- /dev/null +++ b/.github/actions/compile-i18n/action.yml @@ -0,0 +1,15 @@ +--- +name: Compile translations +description: Compile i18n translations and fail on compilation errors. + +runs: + using: composite + steps: + - name: πŸ”€ Compile translations + shell: bash + run: pnpm intl:build 2>&1 | tee i18n.log + + - name: Check for i18n compilation errors + shell: bash + run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation + errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi diff --git a/.github/actions/setup-expo-project/action.yml b/.github/actions/setup-expo-project/action.yml new file mode 100644 index 0000000000..1369ba0cb6 --- /dev/null +++ b/.github/actions/setup-expo-project/action.yml @@ -0,0 +1,47 @@ +--- +name: Setup Expo Project +description: Install dependencies and set up the Expo/EAS CLI for a build. Does not check out the repo. + +inputs: + expo-token: + description: Expo token (EXPO_TOKEN secret) + required: true + eas-version: + description: EAS CLI version to install + required: false + default: '19.0.5' + +runs: + using: composite + steps: + - name: Check for EXPO_TOKEN + shell: bash + env: + EXPO_TOKEN: ${{ inputs.expo-token }} + run: > + if [ -z "$EXPO_TOKEN" ]; then + echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" + exit 1 + fi + + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - name: πŸ”§ Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: package.json + cache: pnpm + + - name: πŸͺ› Setup jq + uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1 + + - name: βš™οΈ Install dependencies + shell: bash + run: pnpm install --frozen-lockfile + + - name: πŸ”¨ Setup Expo CLI + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0 + with: + eas-version: ${{ inputs.eas-version }} + packager: 'pnpm --allow-build=dtrace-provider' + token: ${{ inputs.expo-token }} diff --git a/.github/actions/write-env/action.yml b/.github/actions/write-env/action.yml new file mode 100644 index 0000000000..7cb3464188 --- /dev/null +++ b/.github/actions/write-env/action.yml @@ -0,0 +1,63 @@ +--- +name: Write Environment Variables +description: Write the .env file and google-services.json used by the build. + +inputs: + env-token: + description: Base .env contents (ENV_TOKEN secret) + required: true + sentry-dsn: + description: Sentry DSN (SENTRY_DSN secret) + required: true + bitdrift-api-key: + description: Bitdrift API key (BITDRIFT_API_KEY secret) + required: true + gcp-project-id: + description: GCP project ID (EXPO_PUBLIC_GCP_PROJECT_ID secret) + required: true + google-services-token: + description: google-services.json contents (GOOGLE_SERVICES_TOKEN secret) + required: true + expo-public-env: + description: > + EXPO_PUBLIC_ENV value. Only set for OTA deploys where eas.json isn't used; + for regular builds this is normally handled in eas.json. + required: false + default: '' + +outputs: + release-version: + description: Version from package.json + value: ${{ steps.env.outputs.release-version }} + bundle-identifier: + description: git SHA of HEAD + value: ${{ steps.env.outputs.bundle-identifier }} + +runs: + using: composite + steps: + - name: ✏️ Write environment variables + id: env + shell: bash + env: + ENV_TOKEN: ${{ inputs.env-token }} + SENTRY_DSN: ${{ inputs.sentry-dsn }} + BITDRIFT_API_KEY: ${{ inputs.bitdrift-api-key }} + GCP_PROJECT_ID: ${{ inputs.gcp-project-id }} + GOOGLE_SERVICES_TOKEN: ${{ inputs.google-services-token }} + EXPO_PUBLIC_ENV: ${{ inputs.expo-public-env }} + run: | + echo "$ENV_TOKEN" > .env + # EXPO_PUBLIC_ENV is normally handled in eas.json; only written here for OTA deploys. + if [ -n "$EXPO_PUBLIC_ENV" ]; then + echo "EXPO_PUBLIC_ENV=$EXPO_PUBLIC_ENV" >> .env + fi + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env + echo "release-version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env + echo "bundle-identifier=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env + echo "EXPO_PUBLIC_SENTRY_DSN=$SENTRY_DSN" >> .env + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=$BITDRIFT_API_KEY" >> .env + echo "EXPO_PUBLIC_GCP_PROJECT_ID=$GCP_PROJECT_ID" >> .env + echo "$GOOGLE_SERVICES_TOKEN" > google-services.json diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index 87c7457706..e3e01a48f0 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -10,12 +10,25 @@ on: options: - testflight-android - production + submit: + type: boolean + description: Submit the build to Google Play (disable to only produce the APK artifact) + default: true workflow_call: inputs: profile: type: string description: Build profile to use required: true + submit: + type: boolean + description: Submit the build to Google Play (disable to only produce the APK artifact) + default: true + runner: + type: string + description: Runner for the build job (defaults to Linux-x64-32core) + required: false + default: '' outputs: package-version: description: Version from package.json @@ -56,48 +69,24 @@ permissions: jobs: build: if: github.repository == 'bluesky-social/social-app' - name: Build and Submit Android - runs-on: Linux-x64-32core + name: Build Android + runs-on: ${{ inputs.runner || 'Linux-x64-32core' }} concurrency: group: android-build cancel-in-progress: false outputs: package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }} version-code: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }} - apk-artifact-name: build-${{ steps.timestamp.outputs.time }}.apk steps: - - name: Check for EXPO_TOKEN - run: > - if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then - echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" - exit 1 - fi - - name: ⬇️ Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 5 - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - - - name: πŸ”§ Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - name: πŸ”§ Setup Expo project + uses: ./.github/actions/setup-expo-project with: - node-version-file: package.json - cache: pnpm - - - name: πŸͺ› Setup jq - uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1 - - - name: βš™οΈ Install dependencies - run: pnpm install --frozen-lockfile - - - name: πŸ”¨ Setup Expo CLI - uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0 - with: - eas-version: '19.0.5' - packager: 'pnpm --allow-build=dtrace-provider' - token: ${{ secrets.EXPO_TOKEN }} + expo-token: ${{ secrets.EXPO_TOKEN }} - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: @@ -105,35 +94,26 @@ jobs: java-version: "17" - name: πŸ”€ Compile translations - run: pnpm intl:build 2>&1 | tee i18n.log - - - name: Check for i18n compilation errors - run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation - errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi + uses: ./.github/actions/compile-i18n # EXPO_PUBLIC_ENV is handled in eas.json - - name: Env + - name: ✏️ Write environment variables id: env - run: | - export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' - echo "${{ secrets.ENV_TOKEN }}" > .env - echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env - echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env - echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env - echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env - echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env - echo "$json" > google-services.json + uses: ./.github/actions/write-env + with: + env-token: ${{ secrets.ENV_TOKEN }} + sentry-dsn: ${{ secrets.SENTRY_DSN }} + bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }} + gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} + google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }} - name: πŸ—οΈ EAS Build env: PROFILE: ${{ inputs.profile || 'testflight-android' }} run: > SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} - SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} + SENTRY_RELEASE=${{ steps.env.outputs.release-version }} + SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }} pnpm use-build-number-with-bump pnpm eas build -p android --profile $PROFILE @@ -143,6 +123,39 @@ jobs: id: get-build-info run: bash scripts/setGitHubOutput.sh + # Hands the built bundle off to the submit / universalApk jobs. Retention is + # deliberately short (1 day) since it's only an intra-run handoff artifact. + - name: πŸš€ Upload AAB artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: android-aab-${{ github.run_id }} + retention-days: 1 + if-no-files-found: error + path: build.aab + + submit: + name: Submit to Google Play + runs-on: ubuntu-latest + needs: [build] + # Submit unless explicitly disabled; on events where inputs is empty this still submits. + if: ${{ inputs.submit != false }} + steps: + # eas submit reads app config from the repo, so we need a checkout. + - name: ⬇️ Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 5 + + - name: πŸ”§ Setup Expo project + uses: ./.github/actions/setup-expo-project + with: + expo-token: ${{ secrets.EXPO_TOKEN }} + + - name: ⬇️ Download AAB artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: android-aab-${{ github.run_id }} + - name: πŸš€ Submit to Google Play env: PROFILE: ${{ inputs.profile || 'testflight-android' }} @@ -156,7 +169,43 @@ jobs: webhook-type: incoming-webhook payload-templated: true payload: | - {"text": "Android ${{ inputs.profile || 'testflight-android' }} build submitted to Google Play!\n```Version Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"} + {"text": "Android ${{ inputs.profile || 'testflight-android' }} build submitted to Google Play!\n```Version Number: ${{ needs.build.outputs.package-version }}\nBuild Number: ${{ needs.build.outputs.version-code }}```"} + + # Record the commit only after a successful submit, so a failed submit doesn't + # advance the "most recent testflight" marker. + - name: ⬇️ Restore Cache + id: get-base-commit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + if: ${{ inputs.profile == 'testflight-android' }} + with: + path: most-recent-testflight-commit.txt + key: most-recent-testflight-commit + + - name: ✏️ Write commit hash to cache + if: ${{ inputs.profile == 'testflight-android' }} + env: + GITHUB_SHA: ${{ github.sha }} + run: echo $GITHUB_SHA > most-recent-testflight-commit.txt + + # Runs in parallel with submit: the QA APK shouldn't be blocked by a Play submission failure. + universalApk: + name: Build universal APK + runs-on: ubuntu-latest + needs: [build] + outputs: + apk-artifact-name: build-${{ steps.timestamp.outputs.time }}.apk + steps: + - name: ⬇️ Download AAB artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: android-aab-${{ github.run_id }} + + # bundletool needs a JRE. ubuntu-latest ships a default JDK, but pin it explicitly + # like the build job so the toolchain is deterministic. + - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + distribution: "temurin" + java-version: "17" - name: πŸ”§ Setup bundletool uses: amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1 @@ -164,19 +213,24 @@ jobs: version: "1.18.3" - name: πŸ”‘ Decode keystore - run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > - keystore.jks + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + run: echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > keystore.jks - name: πŸ“¦ Build signed universal APK + env: + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | bundletool build-apks \ --bundle=build.aab \ --output=universal.apks \ --mode=universal \ --ks=keystore.jks \ - --ks-pass=pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \ - --ks-key-alias=${{ secrets.ANDROID_KEY_ALIAS }} \ - --key-pass=pass:${{ secrets.ANDROID_KEY_PASSWORD }} + --ks-pass=pass:"$ANDROID_KEYSTORE_PASSWORD" \ + --ks-key-alias="$ANDROID_KEY_ALIAS" \ + --key-pass=pass:"$ANDROID_KEY_PASSWORD" - name: πŸ“‹ Rename to .zip for extraction run: mv universal.apks universal.zip @@ -204,21 +258,7 @@ jobs: webhook-type: incoming-webhook payload-templated: true payload: | - {"text": "Android ${{ inputs.profile || 'testflight-android' }} APK is ready for testing!\n```Artifact: ${{ steps.upload-artifact.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"} - - - name: ⬇️ Restore Cache - id: get-base-commit - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - if: ${{ inputs.profile == 'testflight-android' }} - with: - path: most-recent-testflight-commit.txt - key: most-recent-testflight-commit - - - name: ✏️ Write commit hash to cache - if: ${{ inputs.profile == 'testflight-android' }} - env: - GITHUB_SHA: ${{ github.sha }} - run: echo $GITHUB_SHA > most-recent-testflight-commit.txt + {"text": "Android ${{ inputs.profile || 'testflight-android' }} APK is ready for testing!\n```Artifact: ${{ steps.upload-artifact.outputs.artifact-url }}\nVersion Number: ${{ needs.build.outputs.package-version }}\nBuild Number: ${{ needs.build.outputs.version-code }}```"} # Releases are cut from tags named after the version (e.g. "1.124.0"), so when a production # build is dispatched against such a tag we attach the APK to the matching release. This runs @@ -226,7 +266,7 @@ jobs: attachToRelease: name: Attach APK to GitHub Release runs-on: ubuntu-latest - needs: [build] + needs: [build, universalApk] if: ${{ inputs.profile == 'production' && github.ref_type == 'tag' && github.repository == 'bluesky-social/social-app' }} permissions: contents: write @@ -254,7 +294,7 @@ jobs: if: ${{ steps.release-check.outputs.exists == 'true' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.build.outputs.apk-artifact-name }} + name: ${{ needs.universalApk.outputs.apk-artifact-name }} - name: 🏷️ Rename APK for release if: ${{ steps.release-check.outputs.exists == 'true' }} diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml index 53f1097251..35d956c62a 100644 --- a/.github/workflows/build-submit-ios.yml +++ b/.github/workflows/build-submit-ios.yml @@ -28,6 +28,11 @@ on: type: string description: TestFlight group to assign the build to after submitting ("none" to skip) default: none + runner: + type: string + description: Runner for the build job (defaults to macos-26-xlarge) + required: false + default: '' outputs: package-version: description: Version from package.json @@ -66,8 +71,8 @@ permissions: jobs: build: if: github.repository == 'bluesky-social/social-app' - name: Build and Submit iOS - runs-on: macos-26-xlarge + name: Build iOS + runs-on: ${{ inputs.runner || 'macos-26-xlarge' }} concurrency: group: ios-build cancel-in-progress: false @@ -75,38 +80,15 @@ jobs: package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }} build-number: ${{ steps.ipa-build-number.outputs.build-number }} steps: - - name: Check for EXPO_TOKEN - run: > - if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then - echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" - exit 1 - fi - - name: ⬇️ Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 5 - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - - - name: πŸ”§ Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - name: πŸ”§ Setup Expo project + uses: ./.github/actions/setup-expo-project with: - node-version-file: package.json - cache: pnpm - - - name: πŸͺ› Setup jq - uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1 - - - name: βš™οΈ Install dependencies - run: pnpm install --frozen-lockfile - - - name: πŸ”¨ Setup Expo CLI - uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0 - with: - eas-version: '19.0.5' - packager: 'pnpm --allow-build=dtrace-provider' - token: ${{ secrets.EXPO_TOKEN }} + expo-token: ${{ secrets.EXPO_TOKEN }} - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: @@ -133,34 +115,26 @@ jobs: key: ${{ runner.os }}-pods-${{ hashFiles('pnpm-lock.yaml') }} - name: πŸ”€ Compile translations - run: pnpm intl:build 2>&1 | tee i18n.log - - - name: Check for i18n compilation errors - run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation - errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi + uses: ./.github/actions/compile-i18n # EXPO_PUBLIC_ENV is handled in eas.json - name: ✏️ Write environment variables id: env - run: | - echo "${{ secrets.ENV_TOKEN }}" > .env - echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env - echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env - echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env - echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env - echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env - echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json + uses: ./.github/actions/write-env + with: + env-token: ${{ secrets.ENV_TOKEN }} + sentry-dsn: ${{ secrets.SENTRY_DSN }} + bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }} + gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} + google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }} - name: πŸ—οΈ EAS Build env: PROFILE: ${{ inputs.profile || 'testflight' }} run: > SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} - SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} + SENTRY_RELEASE=${{ steps.env.outputs.release-version }} + SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }} pnpm use-build-number-with-bump pnpm eas build -p ios --profile $PROFILE @@ -201,16 +175,6 @@ jobs: exit 1 fi - - name: πŸš€ Deploy - run: pnpm eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa" - - - name: πŸͺ² Upload dSYM to Sentry - run: > - SENTRY_ORG=blueskyweb - SENTRY_PROJECT=app - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - pnpm sentry-cli debug-files upload "$BUILD_DIR/Bluesky.app.dSYM.zip" --include-sources - - name: πŸ“š Get version from package.json id: get-build-info run: bash scripts/setGitHubOutput.sh @@ -220,6 +184,7 @@ jobs: # number that actually lands in App Store Connect. `eas build:version:get` reads the # remote counter, which a --local build does not advance, so it can be off by one β€” # using it here would make distribute_only poll for a nonexistent build. + # PlistBuddy is macOS-only, which is why this stays in the build job. - name: πŸ”’ Read build number from IPA id: ipa-build-number run: | @@ -235,18 +200,98 @@ jobs: echo "IPA build number: $build_number" echo "build-number=$build_number" >> "$GITHUB_OUTPUT" + # Hand the IPA and dSYM off to the submit job. Retention is deliberately short since + # this artifact only exists to bridge the two jobs within a single run. + - name: πŸš€ Upload build artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ios-build-${{ github.run_id }} + retention-days: 1 + if-no-files-found: error + path: | + ${{ env.BUILD_DIR }}/Bluesky.ipa + ${{ env.BUILD_DIR }}/Bluesky.app.dSYM.zip + + submit: + name: Submit iOS + # Submission and dSYM upload are I/O bound and don't need the xlarge builder. + runs-on: macos-26 + needs: [build] + steps: + - name: ⬇️ Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # eas submit reads the app config from the repo + fetch-depth: 5 + + - name: πŸ”§ Setup Expo project + uses: ./.github/actions/setup-expo-project + with: + expo-token: ${{ secrets.EXPO_TOKEN }} + + - name: ⬇️ Download build artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ios-build-${{ github.run_id }} + path: ios-build + + - name: πŸš€ Deploy + run: pnpm eas submit -p ios --non-interactive --path ios-build/Bluesky.ipa + + - name: πŸͺ² Upload dSYM to Sentry + env: + SENTRY_ORG: blueskyweb + SENTRY_PROJECT: app + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: pnpm sentry-cli debug-files upload ios-build/Bluesky.app.dSYM.zip --include-sources + + - name: πŸ”” Notify Slack of Production Build + if: ${{ inputs.profile == 'production' }} + uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + with: + webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} + webhook-type: incoming-webhook + payload-templated: true + payload: | + {"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ needs.build.outputs.package-version }}\nBuild Number: ${{ needs.build.outputs.build-number }}```"} + + # Record the commit only after a successful submit, so a failed submit doesn't advance + # the baseline used for the next testflight build's changelog. + - name: ⬇️ Restore Cache + id: get-base-commit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + if: ${{ inputs.profile == 'testflight' }} + with: + path: most-recent-testflight-commit.txt + key: most-recent-testflight-commit + + - name: ✏️ Write commit hash to cache + env: + GITHUB_SHA: ${{ github.sha }} + if: ${{ inputs.profile == 'testflight' }} + run: echo $GITHUB_SHA > most-recent-testflight-commit.txt + + distribute: + name: Assign build to TestFlight group + # fastlane and jq ship preinstalled on the macOS runner image, and this step mostly idles + # polling Apple processing, so it runs on a normal-size runner. + runs-on: macos-26 + needs: [build, submit] + # testFlightGroup defaults to 'none' on both workflow_call and dispatch; guard against the + # empty string too, since `!= 'none'` alone would be true for ''. + if: ${{ inputs.testFlightGroup && inputs.testFlightGroup != 'none' }} + steps: # eas submit only uploads to App Store Connect; it can't assign a build to a # TestFlight group. fastlane's distribute_only mode skips the upload and assigns the # already-submitted build to the group, polling until Apple finishes processing it. - name: πŸ§ͺ Assign build to TestFlight group - if: ${{ inputs.testFlightGroup != 'none' }} env: TESTFLIGHT_GROUP: ${{ inputs.testFlightGroup }} ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }} - APP_VERSION: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }} - BUILD_NUMBER: ${{ steps.ipa-build-number.outputs.build-number }} + APP_VERSION: ${{ needs.build.outputs.package-version }} + BUILD_NUMBER: ${{ needs.build.outputs.build-number }} run: | # Ensure the API key material is removed even if fastlane exits non-zero # (the step runs under `bash -e`, which would otherwise abort before cleanup). @@ -271,27 +316,3 @@ jobs: build_number:"$BUILD_NUMBER" \ groups:"$TESTFLIGHT_GROUP" \ notify_external_testers:true - - - name: πŸ”” Notify Slack of Production Build - if: ${{ inputs.profile == 'production' }} - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 - with: - webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} - webhook-type: incoming-webhook - payload-templated: true - payload: | - {"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.ipa-build-number.outputs.build-number }}```"} - - - name: ⬇️ Restore Cache - id: get-base-commit - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - if: ${{ inputs.profile == 'testflight' }} - with: - path: most-recent-testflight-commit.txt - key: most-recent-testflight-commit - - - name: ✏️ Write commit hash to cache - env: - GITHUB_SHA: ${{ github.sha }} - if: ${{ inputs.profile == 'testflight' }} - run: echo $GITHUB_SHA > most-recent-testflight-commit.txt diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml index acfce80f48..58fcdaf6f9 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -99,11 +99,7 @@ jobs: previous-commit-tag: ${{ inputs.runtimeVersion }} - name: πŸ”€ Compile translations - run: pnpm intl:build 2>&1 | tee i18n.log - - - name: Check for i18n compilation errors - run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation - errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi + uses: ./.github/actions/compile-i18n - name: Lint check run: pnpm lint @@ -128,35 +124,26 @@ jobs: !steps.version.outputs.version-changed }} uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1 - # eas.json not used here, set EXPO_PUBLIC_ENV - - name: Env - env: - CHANNEL: ${{ inputs.channel || 'testflight' }} - GITHUB_SHA: ${{ github.sha }} + # eas.json not used here, so EXPO_PUBLIC_ENV must be written explicitly + - name: ✏️ Write environment variables id: env - if: ${{ !steps.fingerprint.outputs.includes-changes && - !steps.version.outputs.version-changed }} - run: | - export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' - echo "${{ secrets.ENV_TOKEN }}" > .env - echo "EXPO_PUBLIC_ENV=$CHANNEL" >> .env - echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env - echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env - echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env - echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env - echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env - echo "$json" > google-services.json + if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }} + uses: ./.github/actions/write-env + with: + env-token: ${{ secrets.ENV_TOKEN }} + sentry-dsn: ${{ secrets.SENTRY_DSN }} + bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }} + gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} + google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }} + expo-public-env: ${{ inputs.channel || 'testflight' }} - name: πŸ—οΈ Create Bundle if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }} run: > SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} - SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} + SENTRY_RELEASE=${{ steps.env.outputs.release-version }} + SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }} pnpm export - name: πŸ“¦ Package Bundle and πŸš€ Deploy @@ -182,305 +169,64 @@ jobs: !steps.version.outputs.version-changed }} run: echo $GITHUB_SHA > most-recent-testflight-commit.txt - # GitHub actions are horrible so let's just copy paste this in buildIfNecessaryIOS: name: Build and Submit iOS - runs-on: macos-26 - concurrency: - group: ios-build - cancel-in-progress: false needs: [bundleDeploy] # Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be # available here if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app' }} - steps: - - name: Check for EXPO_TOKEN - run: > - if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then - echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" - exit 1 - fi - - - name: ⬇️ Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 5 - - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - - - name: πŸ”§ Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: package.json - cache: pnpm - - - name: πŸ”¨ Setup EAS - uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0 - with: - eas-version: '19.0.5' - packager: 'pnpm --allow-build=dtrace-provider' - token: ${{ secrets.EXPO_TOKEN }} - - - name: βš™οΈ Install dependencies - run: pnpm install --frozen-lockfile - - - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 - with: - xcode-version: "26.4" - - - name: β˜•οΈ Assert Cocoapods version - run: | - EXPECTED=1.17.0 - ACTUAL=$(pod --version) - if [ "$ACTUAL" != "$EXPECTED" ]; then - echo "Expected Cocoapods $EXPECTED but runner has $ACTUAL." - echo "The version ships preinstalled with the macOS runner image: https://github.com/actions/runner-images/blob/main/images/macos/macos-26-Readme.md" - echo "If the runner image changed, update EXPECTED here or reinstall the pinned version." - exit 1 - fi - - - name: πŸ’Ύ Cache Pods - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - id: pods-cache - with: - path: ./ios/Pods - # We'll use the pnpm-lock.yaml for our hash since we don't yet have a Podfile.lock. Pod versions will not - # change unless the pnpm version changes as well. - key: ${{ runner.os }}-pods-${{ hashFiles('pnpm-lock.yaml') }} - - - name: πŸ”€ Compile translations - run: pnpm intl:build - - # EXPO_PUBLIC_ENV is handled in eas.json - - name: Env - id: env - run: | - echo "${{ secrets.ENV_TOKEN }}" > .env - echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env - echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env - echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env - echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env - echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env - echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json - - - name: πŸ—οΈ EAS Build - run: > - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} - SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} - pnpm use-build-number-with-bump - pnpm eas build -p ios - --profile testflight - --local --output build.tar.gz --non-interactive - - - name: πŸ“‚ Extract build artifact - run: | - if [ -f "build.tar.gz" ]; then - echo "Extracting build.tar.gz..." - rm -rf ios-build - mkdir -p ios-build - tar -xzf build.tar.gz -C ios-build - echo "Extraction completed successfully" - - echo "" - echo "Top-level extracted files:" - find ios-build -maxdepth 3 -print - - echo "" - echo "Searching for IPA..." - IPA_PATH="$(find ios-build -type f -name '*.ipa' -print -quit)" - if [ -z "$IPA_PATH" ]; then - echo "ERROR: No .ipa found anywhere under ios-build." - echo "Archive contents:" - tar -tzf build.tar.gz | sed -n '1,200p' - exit 1 - fi - - BUILD_DIR="$(dirname "$IPA_PATH")" - echo "Found IPA at: $IPA_PATH" - echo "Build dir: $BUILD_DIR" - echo "" - echo "Build dir contents:" - ls -la "$BUILD_DIR" - echo "BUILD_DIR=$BUILD_DIR" >> $GITHUB_ENV - else - echo "Archive file not found!" - exit 1 - fi - - - name: πŸš€ Deploy - run: pnpm eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa" - - - name: πŸͺ² Upload dSYM to Sentry - run: > - SENTRY_ORG=blueskyweb - SENTRY_PROJECT=app - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - pnpm sentry-cli debug-files upload "$BUILD_DIR/Bluesky.app.dSYM.zip" --include-sources - - - name: ⬇️ Restore Cache - id: get-base-commit - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - if: ${{ inputs.channel == 'testflight' }} - with: - path: most-recent-testflight-commit.txt - key: most-recent-testflight-commit - - - name: ✏️ Write commit hash to cache - if: ${{ inputs.channel == 'testflight' }} - env: - GITHUB_SHA: ${{ github.sha }} - run: echo $GITHUB_SHA > most-recent-testflight-commit.txt + uses: ./.github/workflows/build-submit-ios.yml + with: + profile: testflight + testFlightGroup: none + # OTA rebuilds don't need the xlarge builder used for releases + runner: macos-26 + # Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`, + # so this workflow never hands the reusable workflow the entire repo secret store. + secrets: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + ENV_TOKEN: ${{ secrets.ENV_TOKEN }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }} + EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} + GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} + ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }} + SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} buildIfNecessaryAndroid: name: Build and Submit Android - runs-on: ubuntu-latest - concurrency: - group: android-build - cancel-in-progress: false needs: [bundleDeploy] # Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be # available here if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected && github.repository == - 'bluesky-social/social-app'}} - - steps: - - name: Check for EXPO_TOKEN - run: > - if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then - echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" - exit 1 - fi - - - name: ⬇️ Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 5 - - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - - - name: πŸ”§ Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: package.json - cache: pnpm - - - name: πŸ”¨ Setup EAS - uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0 - with: - eas-version: '19.0.5' - packager: 'pnpm --allow-build=dtrace-provider' - token: ${{ secrets.EXPO_TOKEN }} - - - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 - with: - distribution: "temurin" - java-version: "17" - - - name: βš™οΈ Install dependencies - run: pnpm install --frozen-lockfile - - - name: πŸ”€ Compile translations - run: pnpm intl:build - - # EXPO_PUBLIC_ENV is handled in eas.json - - name: Env - id: env - run: | - export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' - echo "${{ secrets.ENV_TOKEN }}" > .env - echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env - echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env - echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env - echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env - echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env - echo "$json" > google-services.json - - - name: πŸ—οΈ EAS Build - run: > - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} - SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} - pnpm use-build-number-with-bump - pnpm eas build -p android - --profile testflight-android - --local --output build.aab --non-interactive - - - name: πŸ“š Get version from package.json - id: get-build-info - run: bash scripts/setGitHubOutput.sh - - - name: πŸš€ Submit to Google Play - run: pnpm eas submit -p android --profile testflight-android --non-interactive --path - build.aab - - - name: πŸ”§ Setup bundletool - uses: amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1 - with: - version: "1.18.3" - - - name: πŸ”‘ Decode keystore - run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > - keystore.jks - - - name: πŸ“¦ Build signed universal APK - run: | - bundletool build-apks \ - --bundle=build.aab \ - --output=universal.apks \ - --mode=universal \ - --ks=keystore.jks \ - --ks-pass=pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \ - --ks-key-alias=${{ secrets.ANDROID_KEY_ALIAS }} \ - --key-pass=pass:${{ secrets.ANDROID_KEY_PASSWORD }} - - - name: πŸ“‹ Rename to .zip for extraction - run: mv universal.apks universal.zip - - - name: πŸ“¦ Extract universal APK - run: unzip -p universal.zip universal.apk > build.apk - - - name: ⏰ Get a timestamp - id: timestamp - run: echo "time=$(date -u +'%m-%d-%H-%M-%S')" >> "$GITHUB_OUTPUT" - - - name: πŸš€ Upload Artifact - id: upload-artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - retention-days: 30 - compression-level: 0 - name: build-${{ steps.timestamp.outputs.time }}.apk - path: build.apk - - - name: πŸ”” Notify Slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 - with: - webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} - webhook-type: incoming-webhook - payload-templated: true - payload: | - {"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"} - - - name: ⬇️ Restore Cache - id: get-base-commit - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }} - with: - path: most-recent-testflight-commit.txt - key: most-recent-testflight-commit - - - name: ✏️ Write commit hash to cache - env: - GITHUB_SHA: ${{ github.sha }} - if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }} - run: echo $GITHUB_SHA > most-recent-testflight-commit.txt + 'bluesky-social/social-app' }} + # build-submit-android.yml contains an attachToRelease job that requests contents: write. + # That job is skipped here (it needs a production tag build), but GitHub statically + # validates the reusable-workflow permission ceiling, so the caller must grant it. + permissions: + contents: write + uses: ./.github/workflows/build-submit-android.yml + with: + profile: testflight-android + runner: ubuntu-latest + # Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`, + # so this workflow never hands the reusable workflow the entire repo secret store. + secrets: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + ENV_TOKEN: ${{ secrets.ENV_TOKEN }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }} + EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} + GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} From c4e80b44561fdfa71500267956a474bb6eadb5df Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 20 Jul 2026 17:42:35 +0300 Subject: [PATCH 02/32] Fix Android release asset upload (#11174) --- .github/workflows/build-submit-android.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index e3e01a48f0..212942b01e 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -305,6 +305,7 @@ jobs: if: ${{ steps.release-check.outputs.exists == 'true' }} env: GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} TAG: ${{ github.ref_name }} APK: Bluesky-${{ needs.build.outputs.package-version }}.apk run: | From 0fb7251c64f7f6dc3cf47c7c6000a3b99ef98b5a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 20 Jul 2026 17:42:53 +0300 Subject: [PATCH 03/32] Add nightly Maestro E2E workflow (#11181) Co-authored-by: Claude Fable 5 --- .github/actions/eas-local-build/action.yml | 72 +++ .github/scripts/cleanup-nightly-e2e.sh | 50 +++ .github/scripts/run-nightly-e2e.sh | 179 ++++++++ .github/scripts/summarize-maestro.mjs | 356 +++++++++++++++ .github/workflows/build-submit-android.yml | 19 +- .github/workflows/build-submit-ios.yml | 19 +- .github/workflows/nightly-e2e.yml | 424 ++++++++++++++++++ __e2e__/flows/composer.yml | 16 + __e2e__/flows/feed-reorder.yml | 125 ++++-- __e2e__/flows/onboarding-avatar-creator.yml | 32 ++ __e2e__/flows/onboarding.yml | 29 +- __e2e__/flows/profile-screen-edit.yml | 7 +- .../flows/report-dialog/account.default.yml | 6 +- __e2e__/flows/report-dialog/post.default.yml | 6 +- .../flows/report-dialog/post.edit-reason.yml | 6 +- .../flows/report-dialog/post.reason-other.yml | 6 +- __e2e__/flows/thread-muting.yml | 97 +++- __e2e__/setupApp.yml | 30 +- dev-env/package.json | 3 +- docs/testing.md | 43 ++ eas.json | 11 + package.json | 1 + pnpm-lock.yaml | 3 + src/lib/media/picker.e2e.tsx | 44 ++ src/view/com/testing/TestCtrls.e2e.tsx | 43 +- 25 files changed, 1508 insertions(+), 119 deletions(-) create mode 100644 .github/actions/eas-local-build/action.yml create mode 100755 .github/scripts/cleanup-nightly-e2e.sh create mode 100755 .github/scripts/run-nightly-e2e.sh create mode 100644 .github/scripts/summarize-maestro.mjs create mode 100644 .github/workflows/nightly-e2e.yml diff --git a/.github/actions/eas-local-build/action.yml b/.github/actions/eas-local-build/action.yml new file mode 100644 index 0000000000..ae62b0d572 --- /dev/null +++ b/.github/actions/eas-local-build/action.yml @@ -0,0 +1,72 @@ +--- +name: Local EAS Build +description: Build an Expo app locally with a selected EAS profile. + +inputs: + platform: + description: EAS platform to build (ios or android) + required: true + profile: + description: EAS build profile + required: true + output: + description: Output path for the local build artifact + required: true + log-path: + description: Optional path to tee build output into + required: false + default: "" + bump-build-number: + description: Run the build through use-build-number-with-bump + required: false + default: "false" + sentry-auth-token: + description: Optional Sentry authentication token + required: false + default: "" + sentry-release: + description: Optional Sentry release + required: false + default: "" + sentry-dist: + description: Optional Sentry distribution + required: false + default: "" + +runs: + using: composite + steps: + - name: Build locally with EAS + shell: bash + env: + PLATFORM: ${{ inputs.platform }} + PROFILE: ${{ inputs.profile }} + OUTPUT: ${{ inputs.output }} + LOG_PATH: ${{ inputs.log-path }} + BUMP_BUILD_NUMBER: ${{ inputs.bump-build-number }} + SENTRY_AUTH_TOKEN: ${{ inputs.sentry-auth-token }} + SENTRY_RELEASE: ${{ inputs.sentry-release }} + SENTRY_DIST: ${{ inputs.sentry-dist }} + run: | + set -o pipefail + build_command=( + pnpm eas build + --platform "$PLATFORM" + --profile "$PROFILE" + --local + --output "$OUTPUT" + --non-interactive + ) + + if [ -n "$LOG_PATH" ]; then + mkdir -p "$(dirname "$LOG_PATH")" + if [ "$BUMP_BUILD_NUMBER" = "true" ]; then + pnpm use-build-number-with-bump "${build_command[@]}" 2>&1 | tee "$LOG_PATH" + else + "${build_command[@]}" 2>&1 | tee "$LOG_PATH" + fi + elif [ "$BUMP_BUILD_NUMBER" = "true" ]; then + pnpm use-build-number-with-bump "${build_command[@]}" + else + "${build_command[@]}" + fi diff --git a/.github/scripts/cleanup-nightly-e2e.sh b/.github/scripts/cleanup-nightly-e2e.sh new file mode 100755 index 0000000000..c37974de92 --- /dev/null +++ b/.github/scripts/cleanup-nightly-e2e.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +set +e + +platform="${1:?usage: cleanup-nightly-e2e.sh }" +device_id="${2:-}" +artifact_dir="${GITHUB_WORKSPACE:-$PWD}/artifacts/$platform" +mkdir -p "$artifact_dir" + +if [[ -f i18n.log ]]; then + cp i18n.log "$artifact_dir/i18n.log" +fi + +stop_process_tree() { + local pid="$1" + local child + while read -r child; do + [[ -n "$child" ]] && stop_process_tree "$child" + done < <(pgrep -P "$pid" 2>/dev/null || true) + kill -TERM "$pid" >/dev/null 2>&1 || true +} + +stop_pid_file() { + [[ -f "$1" ]] || return 0 + local pid + pid="$(cat "$1")" + stop_process_tree "$pid" +} + +stop_pid_file "$artifact_dir/logcat.pid" +stop_pid_file "$artifact_dir/metro.pid" +stop_pid_file "$artifact_dir/mock-server.pid" +stop_pid_file "$artifact_dir/emulator.pid" + +if [[ "$platform" == "ios" ]]; then + if [[ -f "$artifact_dir/redis-bin.txt" ]]; then + "$(cat "$artifact_dir/redis-bin.txt")/redis-cli" \ + -h 127.0.0.1 -p 6380 shutdown nosave >/dev/null 2>&1 || true + fi + if [[ -f "$artifact_dir/postgres-bin.txt" ]]; then + "$(cat "$artifact_dir/postgres-bin.txt")/pg_ctl" \ + -D "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" -m fast stop >/dev/null 2>&1 || true + fi + [[ -n "$device_id" ]] && xcrun simctl shutdown "$device_id" >/dev/null 2>&1 || true +else + docker compose -f dev-env/dev-infra/docker-compose.yaml logs --no-color \ + >>"$artifact_dir/docker-services.log" 2>&1 || true + docker compose -f dev-env/dev-infra/docker-compose.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true + [[ -n "$device_id" ]] && adb -s "$device_id" emu kill >/dev/null 2>&1 || true +fi diff --git a/.github/scripts/run-nightly-e2e.sh b/.github/scripts/run-nightly-e2e.sh new file mode 100755 index 0000000000..fe4fb7e355 --- /dev/null +++ b/.github/scripts/run-nightly-e2e.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +platform="${1:?usage: run-nightly-e2e.sh }" +device_id="${2:?usage: run-nightly-e2e.sh }" + +if [[ "$platform" != "ios" && "$platform" != "android" ]]; then + echo "Unsupported platform: $platform" >&2 + exit 2 +fi + +artifact_dir="${GITHUB_WORKSPACE:-$PWD}/artifacts/$platform" +maestro_dir="$artifact_dir/maestro" +mkdir -p "$maestro_dir" + +phase() { + printf '%s\n' "$1" >"$artifact_dir/phase.txt" +} + +wait_for_port() { + local port="$1" + local label="$2" + local attempts="${3:-120}" + + for ((i = 1; i <= attempts; i++)); do + if nc -z 127.0.0.1 "$port" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + + echo "Timed out waiting for $label on port $port" >&2 + return 1 +} + +# shellcheck disable=SC2329 # Invoked through the cleanup trap call chain. +stop_process_tree() { + local pid="$1" + local child + while read -r child; do + [[ -n "$child" ]] && stop_process_tree "$child" + done < <(pgrep -P "$pid" 2>/dev/null || true) + kill -TERM "$pid" >/dev/null 2>&1 || true +} + +# shellcheck disable=SC2329 # Invoked by cleanup, which is registered as a trap. +stop_pid_file() { + local pid_file="$1" + [[ -f "$pid_file" ]] || return 0 + + local pid + pid="$(cat "$pid_file")" + [[ -n "$pid" ]] || return 0 + + # pnpm and Expo both spawn multiple generations of children. + stop_process_tree "$pid" +} + +# shellcheck disable=SC2329 # Invoked by the EXIT/INT/TERM trap below. +cleanup() { + set +e + stop_pid_file "$artifact_dir/logcat.pid" + stop_pid_file "$artifact_dir/metro.pid" + stop_pid_file "$artifact_dir/mock-server.pid" + + if [[ "$platform" == "ios" ]]; then + if [[ -f "$artifact_dir/redis.pid" ]]; then + redis_bin="$(cat "$artifact_dir/redis-bin.txt")" + "$redis_bin/redis-cli" -h 127.0.0.1 -p 6380 shutdown nosave >/dev/null 2>&1 || true + fi + if [[ -f "$artifact_dir/postgres-bin.txt" && -d "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" ]]; then + postgres_bin="$(cat "$artifact_dir/postgres-bin.txt")" + "$postgres_bin/pg_ctl" -D "${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" -m fast stop >/dev/null 2>&1 || true + fi + else + docker compose -f dev-env/dev-infra/docker-compose.yaml logs --no-color \ + >>"$artifact_dir/docker-services.log" 2>&1 || true + docker compose -f dev-env/dev-infra/docker-compose.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true + fi +} + +trap cleanup EXIT INT TERM + +if [[ "$platform" == "android" ]]; then + adb -s "$device_id" logcat -c + adb -s "$device_id" logcat -v threadtime >"$artifact_dir/logcat.log" 2>&1 & + printf '%s\n' "$!" >"$artifact_dir/logcat.pid" +fi + +phase "Starting PostgreSQL, Redis, and mock server" +if [[ "$platform" == "ios" ]]; then + brew install postgresql@14 2>&1 | tee "$artifact_dir/native-dependencies.log" + + postgres_bin="$(brew --prefix postgresql@14)/bin" + redis_version="7.4.7" + redis_archive="${RUNNER_TEMP:-/tmp}/redis-${redis_version}.tar.gz" + redis_source="${RUNNER_TEMP:-/tmp}/redis-${redis_version}" + curl -fsSL -o "$redis_archive" \ + "https://download.redis.io/releases/redis-${redis_version}.tar.gz" + echo "c97e57b0df330a9e091cacff012bebe763c275398cf36ff44cdba876814b595b $redis_archive" \ + | shasum -a 256 --check | tee -a "$artifact_dir/native-dependencies.log" + rm -rf "$redis_source" + tar -xzf "$redis_archive" -C "${RUNNER_TEMP:-/tmp}" + make -C "$redis_source" -j "$(sysctl -n hw.ncpu)" \ + 2>&1 | tee -a "$artifact_dir/native-dependencies.log" + redis_bin="$redis_source/src" + "$redis_bin/redis-server" --version | tee -a "$artifact_dir/native-dependencies.log" + printf '%s\n' "$redis_bin" >"$artifact_dir/redis-bin.txt" + + postgres_data="${RUNNER_TEMP:-/tmp}/nightly-e2e-postgres" + rm -rf "$postgres_data" + "$postgres_bin/initdb" -D "$postgres_data" --auth=trust --username=pg --no-locale \ + >"$artifact_dir/postgres-init.log" 2>&1 + "$postgres_bin/pg_ctl" -D "$postgres_data" \ + -o "-p 5433 -h 127.0.0.1" -l "$artifact_dir/postgres.log" start + printf '%s\n' "$postgres_bin" >"$artifact_dir/postgres-bin.txt" + + "$redis_bin/redis-server" \ + --bind 127.0.0.1 \ + --port 6380 \ + --save "" \ + --appendonly no \ + --daemonize yes \ + --pidfile "$artifact_dir/redis.pid" \ + --logfile "$artifact_dir/redis.log" + + wait_for_port 5433 "PostgreSQL" + wait_for_port 6380 "Redis" + pnpm --dir dev-env start:external >"$artifact_dir/mock-server.log" 2>&1 & +else + pnpm --dir dev-env start >"$artifact_dir/mock-server.log" 2>&1 & +fi +printf '%s\n' "$!" >"$artifact_dir/mock-server.pid" +wait_for_port 1986 "the E2E mock-server manager" + +phase "Starting Metro" +EXPO_PUBLIC_ENV=e2e \ + NODE_ENV=test \ + RN_SRC_EXT=e2e.ts,e2e.tsx \ + pnpm exec expo start --dev-client --clear --port 8081 \ + >"$artifact_dir/metro.log" 2>&1 & +printf '%s\n' "$!" >"$artifact_dir/metro.pid" +wait_for_port 8081 "Metro" + +# Pre-warm Metro bundle so the first Maestro flow doesn't hit a cold-start delay +phase "Pre-warming Metro bundle" +bundle_platform="$platform" +curl -s -o /dev/null "http://localhost:8081/index.bundle?platform=${bundle_platform}&dev=true&minify=false" +echo "Metro bundle pre-warmed for $bundle_platform" + +if [[ "$platform" == "android" ]]; then + phase "Configuring Android localhost routing" + adb -s "$device_id" reverse tcp:3000 tcp:3000 + adb -s "$device_id" reverse tcp:8081 tcp:8081 +fi + +phase "Running Maestro flows" +set +e +maestro test \ + --udid "$device_id" \ + --format JUNIT \ + --output "$artifact_dir/report.xml" \ + --config __e2e__/config.yml \ + --debug-output "$maestro_dir" \ + --test-output-dir "$maestro_dir" \ + --flatten-debug-output \ + __e2e__ \ + 2>&1 | tee "$artifact_dir/maestro-cli.log" +maestro_status=${PIPESTATUS[0]} +set -e + +if [[ "$maestro_status" -eq 0 ]]; then + phase "Completed" +else + phase "Maestro flow failure" +fi + +exit "$maestro_status" diff --git a/.github/scripts/summarize-maestro.mjs b/.github/scripts/summarize-maestro.mjs new file mode 100644 index 0000000000..7154f5b0ee --- /dev/null +++ b/.github/scripts/summarize-maestro.mjs @@ -0,0 +1,356 @@ +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' + +const ENTITY_REPLACEMENTS = { + '&': '&', + ''': "'", + '>': '>', + '<': '<', + '"': '"', +} + +function decodeXml(value = '') { + return value + .replace(/&(amp|apos|gt|lt|quot);/g, entity => ENTITY_REPLACEMENTS[entity]) + .replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))) + .replace(/&#x([\da-f]+);/gi, (_, code) => + String.fromCodePoint(Number.parseInt(code, 16)), + ) +} + +function attributes(source = '') { + const result = {} + for (const match of source.matchAll(/([\w:.-]+)\s*=\s*(["'])(.*?)\2/gs)) { + result[match[1]] = decodeXml(match[3]) + } + return result +} + +function concise(value, limit = 300) { + const normalized = decodeXml(value) + .replace(/<[^>]+>/g, ' ') + .replace(/\s+/g, ' ') + .trim() + return normalized.length > limit + ? `${normalized.slice(0, limit - 1)}…` + : normalized +} + +export function parseJUnit(xml) { + const failures = [] + const testcasePattern = /]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/gi + + for (const match of xml.matchAll(testcasePattern)) { + const testcase = attributes(match[1]) + const body = match[2] || '' + const failure = body.match(/<(failure|error)\b([^>]*)>([\s\S]*?)<\/\1>/i) + const selfClosingFailure = body.match(/<(failure|error)\b([^>]*)\/>/i) + const failureMatch = failure || selfClosingFailure + if (!failureMatch) continue + + const failureAttributes = attributes(failureMatch[2]) + const name = testcase.name || testcase.classname || 'Unnamed Maestro flow' + const message = concise( + failureAttributes.message || (failure ? failureMatch[3] : '') || 'Failed', + ) + failures.push({name, message}) + } + + if (failures.length === 0) { + const suite = xml.match(/]*)>/i) + const suiteAttributes = attributes(suite?.[1]) + if ( + Number(suiteAttributes.failures || 0) + + Number(suiteAttributes.errors || 0) > + 0 + ) { + failures.push({ + name: suiteAttributes.name || 'Maestro test suite', + message: 'JUnit reported a failure without testcase details', + }) + } + } + + return failures +} + +export function parseMaestroCli(log) { + const failures = [] + const failurePattern = /^\[Failed\]\s+(.+?)\s+\([^)]*\)\s+\((.+)\)\s*$/gm + for (const match of log.matchAll(failurePattern)) { + failures.push({ + name: concise(match[1], 120), + message: concise(match[2]), + }) + } + return failures +} + +function walk(root) { + if (!root || !fs.existsSync(root)) return [] + const entries = fs.readdirSync(root, {withFileTypes: true}) + return entries.flatMap(entry => { + const candidate = path.join(root, entry.name) + return entry.isDirectory() ? walk(candidate) : [candidate] + }) +} + +function readPhase(root) { + const phaseFile = walk(root).find(file => path.basename(file) === 'phase.txt') + return phaseFile ? fs.readFileSync(phaseFile, 'utf8').trim() : '' +} + +function platformResult({name, status, root, artifactUrl}) { + const files = walk(root) + const reports = files.filter(file => /(?:report|junit).*\.xml$/i.test(file)) + const junitFailures = reports.flatMap(report => + parseJUnit(fs.readFileSync(report, 'utf8')), + ) + const maestroLogs = files.filter( + file => path.basename(file) === 'maestro-cli.log', + ) + const cliFailures = maestroLogs.flatMap(log => + parseMaestroCli(fs.readFileSync(log, 'utf8')), + ) + // A cancelled or timed-out Maestro run may never flush JUnit. Its CLI log is + // streamed continuously, so use those failure lines when JUnit has no detail. + const failures = junitFailures.length > 0 ? junitFailures : cliFailures + // A skipped platform (e.g. iOS while temporarily disabled) is not a failure + // as long as it produced no flow failures. + const failed = + (status !== 'success' && status !== 'skipped') || failures.length > 0 + return { + name, + status, + failed, + failures, + phase: readPhase(root), + hasJUnit: reports.length > 0, + artifactUrl, + } +} + +function statusEmoji(status) { + if (status === 'success') return ':white_check_mark:' + if (status === 'skipped') return ':fast_forward:' + return ':x:' +} + +function slackEscape(value) { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') +} + +function platformBlock(platform) { + const lines = [ + `${statusEmoji(platform.status)} *${platform.name}* β€” job status: \`${platform.status}\``, + ] + if (platform.failures.length > 0) { + for (const failure of platform.failures.slice(0, 8)) { + lines.push( + `β€’ *${slackEscape(failure.name)}:* ${slackEscape(failure.message)}`, + ) + } + if (platform.failures.length > 8) { + lines.push(`β€’ …and ${platform.failures.length - 8} more failed flows`) + } + } else if (platform.failed && !platform.hasJUnit) { + lines.push( + `β€’ *Setup phase:* ${slackEscape(platform.phase || 'No phase metadata was captured')}`, + ) + } else if (platform.failed) { + lines.push( + `β€’ Job failed after JUnit was written; latest phase: ${slackEscape(platform.phase || 'unknown')}`, + ) + } + if (platform.artifactUrl) { + lines.push( + `β€’ <${platform.artifactUrl}|Open ${platform.name} logs and artifacts>`, + ) + } + return lines.join('\n').slice(0, 3000) +} + +function githubSummary({notify, platforms, shortSha, runUrl, commitUrl}) { + const lines = [ + `# Nightly Maestro E2E ${notify ? 'failed' : 'passed'}`, + '', + `- Commit: [\`${shortSha}\`](${commitUrl})`, + `- Workflow run: [open run](${runUrl})`, + '', + ] + + for (const platform of platforms) { + const headerEmoji = + platform.status === 'success' + ? 'βœ…' + : platform.status === 'skipped' + ? '⏭️' + : '❌' + lines.push( + `## ${headerEmoji} ${platform.name}`, + '', + `Job status: \`${platform.status}\``, + '', + ) + if (platform.failures.length > 0) { + for (const failure of platform.failures.slice(0, 10)) { + lines.push(`- **${failure.name}:** ${failure.message}`) + } + if (platform.failures.length > 10) { + lines.push(`- …and ${platform.failures.length - 10} more failed flows`) + } + lines.push('') + } else if (platform.failed && !platform.hasJUnit) { + lines.push( + `- Setup phase: ${platform.phase || 'No phase metadata was captured'}`, + '', + ) + } else if (platform.failed) { + lines.push( + `- The job failed after JUnit was written (latest phase: ${platform.phase || 'unknown'})`, + '', + ) + } + if (platform.artifactUrl) { + lines.push( + `- [${platform.name} logs and artifacts](${platform.artifactUrl})`, + '', + ) + } + } + + return lines.join('\n').trim() +} + +export function buildSummary({ + iosStatus, + androidStatus, + iosRoot, + androidRoot, + artifactUrls = {}, + sha, + runUrl, + commitUrl, +}) { + const platforms = [ + platformResult({ + name: 'iOS', + status: iosStatus, + root: iosRoot, + artifactUrl: artifactUrls.ios, + }), + platformResult({ + name: 'Android', + status: androidStatus, + root: androidRoot, + artifactUrl: artifactUrls.android, + }), + ] + const notify = platforms.some(platform => platform.failed) + const shortSha = sha.slice(0, 12) + const lines = [ + ':rotating_light: *Nightly Maestro E2E failed*', + `*Commit:* <${commitUrl}|\`${shortSha}\`>`, + `*Workflow run:* <${runUrl}|open run>`, + '', + ] + + for (const platform of platforms) { + lines.push( + `${statusEmoji(platform.status)} *${platform.name}* β€” job status: \`${platform.status}\``, + ) + if (platform.failures.length > 0) { + for (const failure of platform.failures.slice(0, 10)) { + lines.push( + `β€’ *${slackEscape(failure.name)}:* ${slackEscape(failure.message)}`, + ) + } + if (platform.failures.length > 10) { + lines.push(`β€’ …and ${platform.failures.length - 10} more failed flows`) + } + } else if (platform.failed && !platform.hasJUnit) { + lines.push( + `β€’ Setup phase: ${platform.phase || 'No phase metadata was captured'}`, + ) + } else if (platform.failed) { + lines.push( + `β€’ The job failed after JUnit was written (latest phase: ${platform.phase || 'unknown'})`, + ) + } + if (platform.artifactUrl) { + lines.push( + `β€’ <${platform.artifactUrl}|${platform.name} logs and artifacts>`, + ) + } + lines.push('') + } + + const text = lines.join('\n').trim() + const blocks = [ + { + type: 'header', + text: {type: 'plain_text', text: 'Nightly Maestro E2E failed'}, + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*Commit:* <${commitUrl}|\`${shortSha}\`>\n*Workflow run:* <${runUrl}|open run>`, + }, + }, + {type: 'divider'}, + ...platforms.flatMap((platform, index) => [ + {type: 'section', text: {type: 'mrkdwn', text: platformBlock(platform)}}, + ...(index < platforms.length - 1 ? [{type: 'divider'}] : []), + ]), + ] + return { + notify, + platforms, + githubSummary: githubSummary({ + notify, + platforms, + shortSha, + runUrl, + commitUrl, + }), + payload: {text, blocks}, + } +} + +function parseArgs(argv) { + const result = {} + for (let i = 0; i < argv.length; i += 2) { + const key = argv[i] + if (!key?.startsWith('--') || argv[i + 1] === undefined) { + throw new Error(`Invalid argument: ${key || ''}`) + } + result[key.slice(2)] = argv[i + 1] + } + return result +} + +if ( + process.argv[1] && + path.resolve(process.argv[1]) === path.resolve(import.meta.filename) +) { + const args = parseArgs(process.argv.slice(2)) + const artifactUrls = args['artifact-urls'] + ? JSON.parse(fs.readFileSync(args['artifact-urls'], 'utf8')) + : {} + const summary = buildSummary({ + iosStatus: args['ios-status'], + androidStatus: args['android-status'], + iosRoot: args['ios-root'], + androidRoot: args['android-root'], + artifactUrls, + sha: args.sha, + runUrl: args['run-url'], + commitUrl: args['commit-url'], + }) + process.stdout.write(`${JSON.stringify(summary)}\n`) +} diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index 212942b01e..e4a86fc224 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -108,16 +108,15 @@ jobs: google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }} - name: πŸ—οΈ EAS Build - env: - PROFILE: ${{ inputs.profile || 'testflight-android' }} - run: > - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_RELEASE=${{ steps.env.outputs.release-version }} - SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }} - pnpm use-build-number-with-bump - pnpm eas build -p android - --profile $PROFILE - --local --output build.aab --non-interactive + uses: ./.github/actions/eas-local-build + with: + platform: android + profile: ${{ inputs.profile || 'testflight-android' }} + output: build.aab + bump-build-number: "true" + sentry-auth-token: ${{ secrets.SENTRY_AUTH_TOKEN }} + sentry-release: ${{ steps.env.outputs.release-version }} + sentry-dist: ${{ steps.env.outputs.bundle-identifier }} - name: πŸ“š Get version from package.json id: get-build-info diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml index 35d956c62a..5c26977f23 100644 --- a/.github/workflows/build-submit-ios.yml +++ b/.github/workflows/build-submit-ios.yml @@ -129,16 +129,15 @@ jobs: google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }} - name: πŸ—οΈ EAS Build - env: - PROFILE: ${{ inputs.profile || 'testflight' }} - run: > - SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_RELEASE=${{ steps.env.outputs.release-version }} - SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }} - pnpm use-build-number-with-bump - pnpm eas build -p ios - --profile $PROFILE - --local --output build.tar.gz --non-interactive + uses: ./.github/actions/eas-local-build + with: + platform: ios + profile: ${{ inputs.profile || 'testflight' }} + output: build.tar.gz + bump-build-number: "true" + sentry-auth-token: ${{ secrets.SENTRY_AUTH_TOKEN }} + sentry-release: ${{ steps.env.outputs.release-version }} + sentry-dist: ${{ steps.env.outputs.bundle-identifier }} - name: πŸ“‚ Extract build artifact run: | diff --git a/.github/workflows/nightly-e2e.yml b/.github/workflows/nightly-e2e.yml new file mode 100644 index 0000000000..8b06e36764 --- /dev/null +++ b/.github/workflows/nightly-e2e.yml @@ -0,0 +1,424 @@ +--- +name: Nightly Maestro E2E + +on: + schedule: + - cron: "0 4 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: nightly-maestro-e2e-${{ github.ref }} + cancel-in-progress: false + +env: + CI: "1" + MAESTRO_VERSION: "2.6.1" + MAESTRO_DRIVER_STARTUP_TIMEOUT: "180000" + MAESTRO_CLI_NO_ANALYTICS: "1" + MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED: "true" + MAESTRO_DISABLE_UPDATE_CHECK: "1" + +jobs: + ios: + name: iOS Maestro E2E + if: github.repository == 'bluesky-social/social-app' + runs-on: macos-26-xlarge + timeout-minutes: 120 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Select Xcode 26.4 + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 + with: + xcode-version: "26.4" + + - name: Prepare E2E configuration + run: | + mkdir -p artifacts/ios + echo "Installing dependencies" > artifacts/ios/phase.txt + cp .env.example .env.test + cp google-services.json.example google-services.json + + - name: Set up Expo project + uses: ./.github/actions/setup-expo-project + with: + expo-token: ${{ secrets.EXPO_TOKEN }} + + - name: Set up Java 17 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + distribution: temurin + java-version: "17" + + - name: Install dev-env dependencies + run: pnpm --dir dev-env install --frozen-lockfile 2>&1 | tee artifacts/ios/dependencies.log + + - name: Compile translations + uses: ./.github/actions/compile-i18n + + - name: Install Maestro 2.6.1 + run: | + echo "Installing Maestro" > artifacts/ios/phase.txt + curl -fsSL -o "$RUNNER_TEMP/maestro.zip" \ + "https://github.com/mobile-dev-inc/Maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip" + echo "3440825f514f537c6a96bcf5de995780c2a4a7f83a43208fdc95d4f1fecfad3b $RUNNER_TEMP/maestro.zip" \ + | shasum -a 256 --check + unzip -q "$RUNNER_TEMP/maestro.zip" -d "$RUNNER_TEMP" + echo "$RUNNER_TEMP/maestro/bin" >> "$GITHUB_PATH" + "$RUNNER_TEMP/maestro/bin/maestro" --version | tee artifacts/ios/maestro-version.log + test "$("$RUNNER_TEMP/maestro/bin/maestro" --version)" = "$MAESTRO_VERSION" + + - name: Boot one iOS simulator + run: | + echo "Booting iOS simulator" > artifacts/ios/phase.txt + device_name="iPhone 17" + runtime_name="iOS 26.5" + + runtime_id=$(xcrun simctl list runtimes available --json | jq -r \ + --arg name "$runtime_name" \ + '[.runtimes[] | select(.name == $name and .isAvailable != false)] | first | .identifier // empty') + if [ -z "$runtime_id" ]; then + echo "The $runtime_name simulator runtime is not installed. Available iOS runtimes:" >&2 + xcrun simctl list runtimes available --json | jq -r \ + '.runtimes[] | select(.name | startswith("iOS")) | "- \(.name)"' >&2 + exit 1 + fi + + device_type_id=$(xcrun simctl list devicetypes --json | jq -r \ + --arg name "$device_name" \ + '[.devicetypes[] | select(.name == $name)] | first | .identifier // empty') + if [ -z "$device_type_id" ]; then + echo "The $device_name simulator device type is not installed" >&2 + exit 1 + fi + + udid=$(xcrun simctl list devices available --json | jq -r \ + --arg runtime "$runtime_id" \ + --arg name "$device_name" \ + '[.devices[$runtime][]? | select(.name == $name)] | first | .udid // empty') + if [ -z "$udid" ]; then + udid=$(xcrun simctl create "$device_name" "$device_type_id" "$runtime_id") + fi + + echo "IOS_UDID=$udid" >> "$GITHUB_ENV" + xcrun simctl shutdown all || true + xcrun simctl boot "$udid" + xcrun simctl bootstatus "$udid" -b + echo "Using $device_name on $runtime_name ($udid)" + + - name: Mark iOS development client build phase + run: echo "Building the iOS development client" > artifacts/ios/phase.txt + + - name: Build iOS development client + uses: ./.github/actions/eas-local-build + with: + platform: ios + profile: e2e + output: ${{ runner.temp }}/nightly-e2e-ios.tar.gz + log-path: artifacts/ios/build.log + + - name: Install iOS development client + run: | + build_contents="$RUNNER_TEMP/nightly-e2e-ios-build" + mkdir -p "$build_contents" + tar -xzf "$RUNNER_TEMP/nightly-e2e-ios.tar.gz" -C "$build_contents" + app_path=$(find "$build_contents" -type d -name '*.app' -print -quit) + if [ -z "$app_path" ]; then + echo "The local EAS build did not contain an iOS simulator app" >&2 + exit 1 + fi + xcrun simctl install "$IOS_UDID" "$app_path" 2>&1 | tee -a artifacts/ios/build.log + + - name: Run iOS Maestro suite + run: .github/scripts/run-nightly-e2e.sh ios "$IOS_UDID" + + - name: Clean up iOS services and simulator + if: always() + run: .github/scripts/cleanup-nightly-e2e.sh ios "${IOS_UDID:-}" + + - name: Upload iOS E2E artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: nightly-e2e-ios-${{ github.run_id }} + path: artifacts/ios + if-no-files-found: warn + retention-days: 7 + + android: + name: Android Maestro E2E + if: github.repository == 'bluesky-social/social-app' + # Linux-x64-32core is a repository-managed runner label. + runs-on: Linux-x64-32core + timeout-minutes: 120 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Prepare E2E configuration + run: | + mkdir -p artifacts/android + echo "Installing dependencies" > artifacts/android/phase.txt + cp .env.example .env.test + cp google-services.json.example google-services.json + + - name: Set up Expo project + uses: ./.github/actions/setup-expo-project + with: + expo-token: ${{ secrets.EXPO_TOKEN }} + + - name: Set up Java 17 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + distribution: temurin + java-version: "17" + + - name: Install dev-env dependencies + run: pnpm --dir dev-env install --frozen-lockfile 2>&1 | tee artifacts/android/dependencies.log + + - name: Compile translations + uses: ./.github/actions/compile-i18n + + - name: Install Maestro 2.6.1 + run: | + echo "Installing Maestro" > artifacts/android/phase.txt + curl -fsSL -o "$RUNNER_TEMP/maestro.zip" \ + "https://github.com/mobile-dev-inc/Maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip" + echo "3440825f514f537c6a96bcf5de995780c2a4a7f83a43208fdc95d4f1fecfad3b $RUNNER_TEMP/maestro.zip" \ + | shasum -a 256 --check + unzip -q "$RUNNER_TEMP/maestro.zip" -d "$RUNNER_TEMP" + echo "$RUNNER_TEMP/maestro/bin" >> "$GITHUB_PATH" + "$RUNNER_TEMP/maestro/bin/maestro" --version | tee artifacts/android/maestro-version.log + test "$("$RUNNER_TEMP/maestro/bin/maestro" --version)" = "$MAESTRO_VERSION" + + - name: Install and boot one Android emulator + run: | + echo "Booting Android emulator" > artifacts/android/phase.txt + android_sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/usr/local/lib/android/sdk}}" + sdkmanager_bin="$android_sdk/cmdline-tools/latest/bin/sdkmanager" + avdmanager_bin="$android_sdk/cmdline-tools/latest/bin/avdmanager" + # API 35 emulator images have known stability problems in headless CI + # (see flutter/flutter#153445); the qemu process died deterministically + # on the first native stack-screen push with the API 35 image. + system_image="system-images;android-34;google_apis;x86_64" + + if [ ! -x "$sdkmanager_bin" ] || [ ! -x "$avdmanager_bin" ]; then + echo "Android command-line tools were not found under $android_sdk" >&2 + find "$android_sdk/cmdline-tools" -maxdepth 3 -type f \( \ + -name sdkmanager -o -name avdmanager \ + \) -print >&2 || true + exit 1 + fi + + export ANDROID_HOME="$android_sdk" + export ANDROID_SDK_ROOT="$android_sdk" + export PATH="$android_sdk/platform-tools:$android_sdk/emulator:$PATH" + echo "ANDROID_HOME=$android_sdk" >> "$GITHUB_ENV" + echo "ANDROID_SDK_ROOT=$android_sdk" >> "$GITHUB_ENV" + echo "$android_sdk/platform-tools" >> "$GITHUB_PATH" + echo "$android_sdk/emulator" >> "$GITHUB_PATH" + echo "Using Android SDK at $android_sdk" + + yes | "$sdkmanager_bin" --sdk_root="$android_sdk" --licenses >/dev/null || true + "$sdkmanager_bin" --sdk_root="$android_sdk" \ + "platform-tools" "emulator" "$system_image" + + export ANDROID_AVD_HOME="$RUNNER_TEMP/.android/avd" + mkdir -p "$ANDROID_AVD_HOME" + echo "ANDROID_AVD_HOME=$ANDROID_AVD_HOME" >> "$GITHUB_ENV" + echo no | "$avdmanager_bin" create avd \ + --force \ + --name nightly-e2e \ + --package "$system_image" \ + --device pixel_6 + + # Reduce resolution to lighten the SwiftShader software-rendering + # workload, and raise RAM/cores/heap so the debug RN app has headroom. + # The default 2560MB RAM led to silent qemu crashes mid-flow. + printf 'hw.lcd.width=720\nhw.lcd.height=1600\nhw.lcd.density=280\nhw.ramSize=6144\nhw.cpu.ncore=4\nvm.heapSize=512\n' \ + >> "$ANDROID_AVD_HOME/nightly-e2e.avd/config.ini" + + if [ -e /dev/kvm ] && [ ! -w /dev/kvm ]; then + sudo chmod 666 /dev/kvm + fi + + # Disable the emulator's Vulkan feature so graphics goes through the + # plain GLES SwiftShader translator. gfxstream Vulkan via SwiftShader + # Subzero crashed qemu silently at a deterministic rendering step; + # GLES-only is sufficient since the guest renders with skiagl. + # + # Run the launch in a background subshell so the emulator's exit + # status is recorded when it dies (it is otherwise backgrounded and + # its death is invisible). Write the emulator's real PID - not the + # subshell's - to emulator.pid, since cleanup-nightly-e2e.sh kills the + # PID from that file directly; killing the subshell would not kill the + # emulator child. + ( + # wait returns the emulator's non-zero status on crash; set -e would + # abort the subshell before the status is logged. + set +e + "$android_sdk/emulator/emulator" @nightly-e2e \ + -port 5554 \ + -no-window \ + -gpu swiftshader_indirect \ + -feature -Vulkan \ + -no-snapshot \ + -noaudio \ + -no-boot-anim \ + -camera-back none \ + > artifacts/android/emulator.log 2>&1 & + emulator_pid=$! + echo "$emulator_pid" > artifacts/android/emulator.pid + wait "$emulator_pid" + echo "Emulator exited with status $?" >> artifacts/android/emulator.log + ) & + + adb -s emulator-5554 wait-for-device + booted=false + for _ in $(seq 1 120); do + if [ "$(adb -s emulator-5554 shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; then + booted=true + break + fi + sleep 5 + done + if [ "$booted" != "true" ]; then + echo "Android emulator did not finish booting" >&2 + exit 1 + fi + + adb -s emulator-5554 shell settings put global window_animation_scale 0 + adb -s emulator-5554 shell settings put global transition_animation_scale 0 + adb -s emulator-5554 shell settings put global animator_duration_scale 0 + + - name: Mark Android development client build phase + run: echo "Building the Android development client" > artifacts/android/phase.txt + + - name: Build Android development client + uses: ./.github/actions/eas-local-build + with: + platform: android + profile: e2e + output: ${{ runner.temp }}/nightly-e2e-android.apk + log-path: artifacts/android/build.log + + - name: Install Android development client + run: | + adb -s emulator-5554 install -r "$RUNNER_TEMP/nightly-e2e-android.apk" \ + 2>&1 | tee -a artifacts/android/build.log + + - name: Run Android Maestro suite + run: .github/scripts/run-nightly-e2e.sh android emulator-5554 + + - name: Capture emulator crash diagnostics + if: always() + run: | + { + echo "=== Emulator process status ===" + pgrep -fa "emulator.*nightly-e2e" || echo "Emulator process not found" + echo "=== Emulator exit status ===" + grep "Emulator exited" artifacts/android/emulator.log || echo "No emulator exit status recorded" + echo "=== OOM killer check (kernel) ===" + oom_lines=$(sudo dmesg 2>/dev/null | grep -iE "oom|killed process|out of memory" | tail -20) + echo "${oom_lines:-No kernel OOM evidence found (or dmesg unavailable)}" + echo "=== systemd-oomd check ===" + oomd_lines=$(journalctl -u systemd-oomd --no-pager 2>/dev/null | tail -20) + echo "${oomd_lines:-No systemd-oomd journal entries (or journalctl unavailable)}" + echo "=== journal kernel tail ===" + journalctl -k --no-pager 2>/dev/null | tail -30 || echo "journalctl -k unavailable" + echo "=== Emulator crash database ===" + ls -la /tmp/android-runner/emu-crash-*.db 2>/dev/null || echo "No crash database found" + } > artifacts/android/emulator-diagnostics.log 2>&1 + + - name: Clean up Android services and emulator + if: always() + run: .github/scripts/cleanup-nightly-e2e.sh android emulator-5554 + + - name: Upload Android E2E artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: nightly-e2e-android-${{ github.run_id }} + path: artifacts/android + if-no-files-found: warn + retention-days: 7 + + report: + name: Report E2E failures + needs: [ios, android] + if: ${{ always() && github.repository == 'bluesky-social/social-app' }} + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Download iOS artifacts + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: nightly-e2e-ios-${{ github.run_id }} + path: downloaded-artifacts/ios + + - name: Download Android artifacts + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: nightly-e2e-android-${{ github.run_id }} + path: downloaded-artifacts/android + + - name: Resolve artifact links + env: + GH_TOKEN: ${{ github.token }} + run: | + run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}#artifacts" + jq -n --arg run "$run_url" '{ios: $run, android: $run}' > artifact-links.json + if gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \ + > artifact-response.json; then + jq --arg base "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts" \ + --arg run "$run_url" \ + '{ + ios: (.artifacts | map(select(.name | startswith("nightly-e2e-ios-"))) | first | if . then ($base + "/" + (.id | tostring)) else $run end), + android: (.artifacts | map(select(.name | startswith("nightly-e2e-android-"))) | first | if . then ($base + "/" + (.id | tostring)) else $run end) + }' artifact-response.json > artifact-links.json + fi + + - name: Summarize platform results + id: summary + env: + IOS_STATUS: ${{ needs.ios.result }} + ANDROID_STATUS: ${{ needs.android.result }} + run: | + node .github/scripts/summarize-maestro.mjs \ + --ios-status "$IOS_STATUS" \ + --android-status "$ANDROID_STATUS" \ + --ios-root downloaded-artifacts/ios \ + --android-root downloaded-artifacts/android \ + --artifact-urls artifact-links.json \ + --sha "$GITHUB_SHA" \ + --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + --commit-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}" \ + > e2e-summary.json + echo "notify=$(jq -r .notify e2e-summary.json)" >> "$GITHUB_OUTPUT" + echo "payload=$(jq -c .payload e2e-summary.json)" >> "$GITHUB_OUTPUT" + jq -r .githubSummary e2e-summary.json >> "$GITHUB_STEP_SUMMARY" + + - name: Notify Slack of E2E failures + if: steps.summary.outputs.notify == 'true' + uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + with: + webhook: ${{ secrets.E2E_FAILURES_SLACK_WEBHOOK }} + webhook-type: incoming-webhook + payload: ${{ steps.summary.outputs.payload }} diff --git a/__e2e__/flows/composer.yml b/__e2e__/flows/composer.yml index 62f195de75..e348ec1b25 100644 --- a/__e2e__/flows/composer.yml +++ b/__e2e__/flows/composer.yml @@ -44,6 +44,12 @@ appId: xyz.blueskyweb.app id: "e2eRefreshHome" - tapOn: id: "replyBtn" +# Wait for the composer to fully open before typing. Tapping replyBtn right +# after the previous publish can race the closing composer on Android. +- extendedWaitUntil: + visible: + id: "composerPublishBtn" + timeout: 10000 - inputText: "Reply text only" - tapOn: id: "composerPublishBtn" @@ -51,6 +57,11 @@ appId: xyz.blueskyweb.app id: "composeFAB" - tapOn: id: "replyBtn" +# Wait for the composer to fully open before typing. +- extendedWaitUntil: + visible: + id: "composerPublishBtn" + timeout: 10000 - inputText: "Reply with an image" - tapOn: id: "openMediaBtn" @@ -63,6 +74,11 @@ appId: xyz.blueskyweb.app id: "composeFAB" - tapOn: id: "replyBtn" +# Wait for the composer to fully open before typing. +- extendedWaitUntil: + visible: + id: "composerPublishBtn" + timeout: 10000 - inputText: "Reply with a https://example.com link card" - tapOn: id: "composerPublishBtn" diff --git a/__e2e__/flows/feed-reorder.yml b/__e2e__/flows/feed-reorder.yml index 42abc3295e..1e2fe70d62 100644 --- a/__e2e__/flows/feed-reorder.yml +++ b/__e2e__/flows/feed-reorder.yml @@ -29,51 +29,88 @@ appId: xyz.blueskyweb.app id: "homeScreenFeedTabs-selector-1" text: "alice-favs" -# Set alice-favs first -- tapOn: "Open drawer menu" -- tapOn: - id: "menuItemButton-Feeds" -- tapOn: - id: "editFeedsBtn" -- swipe: - label: "Drag feed down" - from: - id: "feed-drag-handle" - direction: "DOWN" - duration: 1000 -- tapOn: - label: "Save button" - id: "saveChangesBtn" -- tapOn: "Go back" -- assertVisible: - id: "homeScreenFeedTabs-selector-0" - text: "alice-favs" -- assertVisible: - id: "homeScreenFeedTabs-selector-1" - text: "Following" +# Reordering feeds is driven by a drag on the feed-drag-handle. Maestro cannot +# activate the RNGH Pan gesture from a synthetic swipe on Android (proven +# twice - coordinate swipes never register the pan), so the reorder +# verification below runs on iOS only. If Android drag coverage is needed, +# revisit with the SavedFeedsA11y move buttons rather than a swipe. +- runFlow: + when: + platform: iOS + commands: + # Set alice-favs first + - tapOn: "Open drawer menu" + - tapOn: + id: "menuItemButton-Feeds" + - tapOn: + id: "editFeedsBtn" + - swipe: + label: "Drag feed down" + from: + id: "feed-drag-handle" + direction: "DOWN" + duration: 1000 + - assertVisible: + id: "saveChangesBtn" + enabled: true + - tapOn: + label: "Save button" + id: "saveChangesBtn" + - tapOn: "Go back" + - assertVisible: + id: "homeScreenFeedTabs-selector-0" + text: "alice-favs" + - assertVisible: + id: "homeScreenFeedTabs-selector-1" + text: "Following" -# Set following first -- tapOn: "Open drawer menu" -- tapOn: - id: "menuItemButton-Feeds" -- tapOn: - id: "editFeedsBtn" -- swipe: - label: "Drag feed down" - from: - id: "feed-drag-handle" - direction: "DOWN" - duration: 1000 -- tapOn: - label: "Save button" - id: "saveChangesBtn" -- tapOn: "Go back" -- assertVisible: - id: "homeScreenFeedTabs-selector-0" - text: "Following" -- assertVisible: - id: "homeScreenFeedTabs-selector-1" - text: "alice-favs" + # Set following first + - tapOn: "Open drawer menu" + - tapOn: + id: "menuItemButton-Feeds" + - tapOn: + id: "editFeedsBtn" + - swipe: + label: "Drag feed down" + from: + id: "feed-drag-handle" + direction: "DOWN" + duration: 1000 + - assertVisible: + id: "saveChangesBtn" + enabled: true + - tapOn: + label: "Save button" + id: "saveChangesBtn" + - tapOn: "Go back" + - assertVisible: + id: "homeScreenFeedTabs-selector-0" + text: "Following" + - assertVisible: + id: "homeScreenFeedTabs-selector-1" + text: "alice-favs" + +# On Android, the reorder path above is skipped. Smoke-test that the feeds +# edit screen opens and the pinned feeds render, then return to a valid state. +- runFlow: + when: + platform: Android + commands: + - tapOn: "Open drawer menu" + - tapOn: + id: "menuItemButton-Feeds" + - tapOn: + id: "editFeedsBtn" + - assertVisible: "Following" + - assertVisible: "alice-favs" + # Two back presses to reach Home: the first pops the saved-feeds editor + # back to the Feeds screen, the second pops Feeds back to Home. On iOS the + # equivalent path saves changes first (saveChangesBtn calls + # navigation.goBack), so a single "Go back" there already lands on Home. + # This Android smoke branch never saves, so it needs the extra pop to + # leave the screen state on Home, which the shared steps below expect. + - tapOn: "Go back" + - tapOn: "Go back" # Remove following - tapOn: "Open drawer menu" diff --git a/__e2e__/flows/onboarding-avatar-creator.yml b/__e2e__/flows/onboarding-avatar-creator.yml index 2204abb9df..7b65c405bb 100644 --- a/__e2e__/flows/onboarding-avatar-creator.yml +++ b/__e2e__/flows/onboarding-avatar-creator.yml @@ -15,6 +15,24 @@ appId: xyz.blueskyweb.app - tapOn: id: "e2eStartOnboarding" - tapOn: "Open avatar creator" +# The avatar-creator bottom sheet (Dialog.Inner, non-scrollable) opens only +# half-expanded on the short E2E emulator (720x1600), a ~220px sliver with the +# emoji grid below the fold. It is NOT a scroll view, so scrollUntilVisible's +# swipe grabs the sheet's own drag gesture and flings it closed. Instead, drag +# the sheet upward to expand it to full height, which brings the picker into +# view. iOS opens the sheet fully already, so this is Android-only. +- runFlow: + when: + platform: Android + commands: + - swipe: + label: "Drag the bottom sheet up to expand it" + start: "50%, 90%" + end: "50%, 20%" + duration: 600 + - extendedWaitUntil: + visible: "Select an emoji" + timeout: 10000 - tapOn: "Select the zap emoji as your avatar" - tapOn: label: "Tap on yellow" @@ -22,6 +40,20 @@ appId: xyz.blueskyweb.app - tapOn: "Done" - waitForAnimationToEnd - tapOn: "Select an avatar" +# Reopening the creator sheet lands on the same half-expanded sliver on +# Android, so expand it again before reaching for the emoji grid. No-op on iOS. +- runFlow: + when: + platform: Android + commands: + - swipe: + label: "Drag the bottom sheet up to expand it" + start: "50%, 90%" + end: "50%, 20%" + duration: 600 + - extendedWaitUntil: + visible: "Select an emoji" + timeout: 10000 - tapOn: "Select the atom emoji as your avatar" - tapOn: "Done" - waitForAnimationToEnd diff --git a/__e2e__/flows/onboarding.yml b/__e2e__/flows/onboarding.yml index a5f4217455..4c0ffdd48f 100644 --- a/__e2e__/flows/onboarding.yml +++ b/__e2e__/flows/onboarding.yml @@ -16,13 +16,28 @@ appId: xyz.blueskyweb.app id: "e2eStartOnboarding" - tapOn: "Select an avatar" - waitForAnimationToEnd -- assertVisible: "Photos" -- assertVisible: "Collections" -- tapOn: - point: "50%,22%" -- waitForAnimationToEnd -- tapOn: "Done" -- waitForAnimationToEnd +- runFlow: + when: + platform: iOS + commands: + - assertVisible: "Photos" + - assertVisible: "Collections" + - tapOn: + point: "50%,22%" + - waitForAnimationToEnd + - tapOn: "Done" + - waitForAnimationToEnd +- runFlow: + when: + platform: Android + commands: + # The system photo picker opened here shows MediaStore photos, which + # the e2e run doesn't seed (media is seeded into app-scoped storage for + # the composer's mocked picker instead). With no photo to pick, dismiss + # the picker and continue - onContinue falls back to the generated + # placeholder avatar, and nothing later in the flow depends on the image. + - back + - waitForAnimationToEnd - tapOn: id: "onboardingContinue" - assertVisible: "What are your interests?" diff --git a/__e2e__/flows/profile-screen-edit.yml b/__e2e__/flows/profile-screen-edit.yml index f60ca01b42..f029ee691a 100644 --- a/__e2e__/flows/profile-screen-edit.yml +++ b/__e2e__/flows/profile-screen-edit.yml @@ -45,7 +45,7 @@ appId: xyz.blueskyweb.app id: "editProfileSaveBtn" - assertNotVisible: id: "editProfileModal" -- assertVisible: "Alicia" +- assertVisible: ".*Alicia.*" - assertVisible: "One cool hacker" # Remove display name and description via the edit profile modal @@ -64,7 +64,10 @@ appId: xyz.blueskyweb.app id: "editProfileSaveBtn" - assertNotVisible: id: "editProfileModal" -- assertVisible: "alice.test" +# The display-name node renders the handle as a Text with a nested badge View +# once the display name is cleared, so the a11y text is not the bare handle +# string on Android. Match it as a substring instead. +- assertVisible: ".*alice\\.test.*" - assertNotVisible: "One cool hacker" # Set avi and banner via the edit profile modal diff --git a/__e2e__/flows/report-dialog/account.default.yml b/__e2e__/flows/report-dialog/account.default.yml index 372fc31c7e..c5c5169cdc 100644 --- a/__e2e__/flows/report-dialog/account.default.yml +++ b/__e2e__/flows/report-dialog/account.default.yml @@ -22,5 +22,7 @@ appId: xyz.blueskyweb.app text: "Send report to Dev-env Moderation" - tapOn: id: "report:submit" -- assertNotVisible: - id: "report:dialog" +- extendedWaitUntil: + notVisible: + id: "report:dialog" + timeout: 20000 diff --git a/__e2e__/flows/report-dialog/post.default.yml b/__e2e__/flows/report-dialog/post.default.yml index be3ac6b68a..212accc254 100644 --- a/__e2e__/flows/report-dialog/post.default.yml +++ b/__e2e__/flows/report-dialog/post.default.yml @@ -22,5 +22,7 @@ appId: xyz.blueskyweb.app text: "Send report to Dev-env Moderation" - tapOn: id: "report:submit" -- assertNotVisible: - id: "report:dialog" +- extendedWaitUntil: + notVisible: + id: "report:dialog" + timeout: 20000 diff --git a/__e2e__/flows/report-dialog/post.edit-reason.yml b/__e2e__/flows/report-dialog/post.edit-reason.yml index eec5794c4f..a44e7f97d7 100644 --- a/__e2e__/flows/report-dialog/post.edit-reason.yml +++ b/__e2e__/flows/report-dialog/post.edit-reason.yml @@ -39,5 +39,7 @@ appId: xyz.blueskyweb.app text: Your report will be sent to Dev-env Moderation.* - tapOn: id: "report:submit" -- assertNotVisible: - id: "report:dialog" +- extendedWaitUntil: + notVisible: + id: "report:dialog" + timeout: 20000 diff --git a/__e2e__/flows/report-dialog/post.reason-other.yml b/__e2e__/flows/report-dialog/post.reason-other.yml index e1065ece4d..78f5a195ec 100644 --- a/__e2e__/flows/report-dialog/post.reason-other.yml +++ b/__e2e__/flows/report-dialog/post.reason-other.yml @@ -29,5 +29,7 @@ appId: xyz.blueskyweb.app - hideKeyboard - tapOn: id: "report:submit" -- assertNotVisible: - id: "report:dialog" +- extendedWaitUntil: + notVisible: + id: "report:dialog" + timeout: 20000 diff --git a/__e2e__/flows/thread-muting.yml b/__e2e__/flows/thread-muting.yml index 2724833feb..f00d097e27 100644 --- a/__e2e__/flows/thread-muting.yml +++ b/__e2e__/flows/thread-muting.yml @@ -20,6 +20,12 @@ appId: xyz.blueskyweb.app - inputText: "Test thread" - tapOn: id: "composerPublishBtn" +# Wait for the composer to close and the home feed to settle before signing +# out. Without a settle guard the next action can race the closing composer. +- extendedWaitUntil: + visible: + id: "composeFAB" + timeout: 10000 # Login, reply to the thread, and log out - tapOn: @@ -31,9 +37,19 @@ appId: xyz.blueskyweb.app id: "viewHeaderHomeFeedPrefsBtn" - tapOn: id: "replyBtn" +# Wait for the composer to fully open before typing. +- extendedWaitUntil: + visible: + id: "composerPublishBtn" + timeout: 10000 - inputText: "Reply 1" - tapOn: id: "composerPublishBtn" +# Wait for the composer to close before signing out. +- extendedWaitUntil: + visible: + id: "composeFAB" + timeout: 10000 # Login, confirm notification exists, mute thread, and log out - tapOn: @@ -45,10 +61,8 @@ appId: xyz.blueskyweb.app id: "viewHeaderHomeFeedPrefsBtn" - tapOn: id: "bottomBarNotificationsBtn" -- assertVisible: - id: "feedItem-by-bob.test" -- tapOn: - id: "feedItem-by-bob.test" +- assertVisible: ".*Reply 1.*" +- tapOn: ".*Reply 1.*" - tapOn: id: "postDropdownBtn" childOf: @@ -67,16 +81,78 @@ appId: xyz.blueskyweb.app id: "bottomBarProfileBtn" - tapOn: id: "profilePager-selector-1" -- tapOn: - id: "replyBtn" +# Both replies target the thread root ("Test thread" by alice), which sits at +# the top of bob's Replies tab. That tab renders each post in the thread with +# its own replyBtn, so scope the tap to the root post's card +# (feedItem-by-alice.test) rather than relying on which replyBtn Maestro picks +# first. This keeps both reply taps deterministic regardless of list order or +# how many posts have rendered. +# +# Even with the close-gating below, the replyBtn tap can land on a recycled list +# row while the author feed re-renders after a publish, and be swallowed so the +# composer never opens. Wrapping the tap + open-wait in retry makes opening the +# composer idempotent: a swallowed tap just re-taps until the publish button +# appears. A first-try success does not retry. +- retry: + maxRetries: 3 + commands: + - tapOn: + id: "replyBtn" + childOf: + id: "feedItem-by-alice.test" + # Wait for the composer to fully open before typing. + - extendedWaitUntil: + visible: + id: "composerPublishBtn" + timeout: 10000 - inputText: "Reply 2" - tapOn: id: "composerPublishBtn" -- tapOn: - id: "replyBtn" +# Wait for the composer to actually close before opening it again. replyBtn +# stays in the accessibility tree behind the open composer sheet, so waiting on +# its visibility returns immediately and does not gate on the close animation or +# the author-feed re-render that follows a post - the next replyBtn tap then +# fires mid-transition and is swallowed, so the composer never opens. Gate on +# the publish button disappearing (the composer is gone), then confirm the +# reply button underneath is back and let animations settle. +- extendedWaitUntil: + notVisible: + id: "composerPublishBtn" + timeout: 15000 +- extendedWaitUntil: + visible: + id: "replyBtn" + timeout: 10000 +- waitForAnimationToEnd +# As with Reply 2: even after gating on the composer close, this tap can hit a +# recycled row during the post-publish feed re-render and be swallowed, so wrap +# the open in retry to make it idempotent. +- retry: + maxRetries: 3 + commands: + - tapOn: + id: "replyBtn" + childOf: + id: "feedItem-by-alice.test" + # Wait for the composer to fully open before typing. + - extendedWaitUntil: + visible: + id: "composerPublishBtn" + timeout: 10000 - inputText: "Reply 3" - tapOn: id: "composerPublishBtn" +# Wait for the composer to actually close before signing out. As above, +# replyBtn stays visible behind the sheet, so gate on the publish button +# disappearing first, then confirm the reply button underneath has returned. +- extendedWaitUntil: + notVisible: + id: "composerPublishBtn" + timeout: 15000 +- extendedWaitUntil: + visible: + id: "replyBtn" + timeout: 10000 # Login, confirm notifications dont exist, unmute the thread, ~~confirm notifications exist~~ # Mute thread behaviour no longer change old notifications after muting/unmuting a thread -sfn @@ -92,10 +168,7 @@ appId: xyz.blueskyweb.app - assertVisible: ".*Reply 1.*" - assertNotVisible: ".*Reply 2.*" - assertNotVisible: ".*Reply 3.*" -- assertVisible: - id: "feedItem-by-bob.test" -- tapOn: - id: "feedItem-by-bob.test" +- tapOn: ".*Reply 1.*" - tapOn: id: "postDropdownBtn" childOf: diff --git a/__e2e__/setupApp.yml b/__e2e__/setupApp.yml index dc784e661f..ab9623ffba 100644 --- a/__e2e__/setupApp.yml +++ b/__e2e__/setupApp.yml @@ -9,23 +9,27 @@ appId: xyz.blueskyweb.app when: platform: iOS commands: - - openLink: "exp+bluesky://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081" - - runFlow: - when: - visible: 'Open in "Bluesky"' - commands: - - tapOn: Open + - extendedWaitUntil: + visible: "http://localhost:8081" + timeout: 60000 + - tapOn: "http://localhost:8081" - runFlow: when: platform: Android commands: - - tapOn: 'http://localhost:8081' - - runFlow: - label: "Dismiss Expo dev menu" - when: - visible: "Continue" - commands: - - back + - extendedWaitUntil: + visible: "http://10.0.2.2:8081" + timeout: 60000 + - tapOn: "http://10.0.2.2:8081" + - extendedWaitUntil: + visible: "Continue" + timeout: 180000 + - tapOn: "Continue" + - back +- extendedWaitUntil: + visible: + id: e2eProxyHeaderInput + timeout: 180000 - tapOn: id: e2eProxyHeaderInput - inputText: ${output.result} diff --git a/dev-env/package.json b/dev-env/package.json index e77ad1dfab..c65ca9cd64 100644 --- a/dev-env/package.json +++ b/dev-env/package.json @@ -3,7 +3,8 @@ "version": "0.0.0", "type": "module", "scripts": { - "start": "NODE_ENV=development ./dev-infra/with-test-redis-and-db.sh node ./mock-server.ts" + "start": "NODE_ENV=development ./dev-infra/with-test-redis-and-db.sh node ./mock-server.ts", + "start:external": "NODE_ENV=development PGPORT=5433 PGHOST=localhost PGUSER=pg PGPASSWORD=password PGDATABASE=postgres DB_POSTGRES_URL=postgresql://pg:password@127.0.0.1:5433/postgres REDIS_HOST=127.0.0.1:6380 node ./mock-server.ts" }, "dependencies": { "@atproto/api": "^0.20.22", diff --git a/docs/testing.md b/docs/testing.md index b84d966112..1bc2af46c5 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -29,6 +29,49 @@ adb reverse tcp:3000 tcp:3000 - In a second tab, run `pnpm e2e:build` - In a third tab, run `pnpm e2e:run __e2e__` +## Nightly Maestro CI + +The `Nightly Maestro E2E` GitHub Actions workflow runs every day at 04:00 UTC +and can also be started from the Actions tab with **Run workflow**. It runs iOS +and Android concurrently, but each platform runs all of `__e2e__/config.yml` +sequentially on one explicitly selected simulator or emulator. The flows share a +stateful mock-server manager, so the suite must not be sharded. + +The jobs run Maestro CLI 2.6.1 locally on GitHub Actions; Maestro Cloud is not +used. iOS runs on `macos-26-xlarge` with Xcode 26.4. Android runs on +`Linux-x64-32core`. Both use Java 17 and the Node and pnpm versions declared in +`package.json`. The iOS job selects an iPhone 17 simulator running iOS 26.5; +Android directly provisions and boots a Pixel 6 AVD with the API 35 Google APIs +x86_64 image using the Android SDK command-line tools. +Both development clients use the `e2e` EAS profile and the same reusable local +EAS build action as the release build workflows; the resulting simulator app +and APK are installed directly on the selected devices. + +The mock-server manager listens on host port 1986 and creates test services on +port 3000. Metro listens on 8081. Android reverses ports 3000 and 8081 into the +emulator; port 1986 remains host-side because Maestro JavaScript calls it from +the runner. Android uses the existing Docker Compose PostgreSQL 14 and Redis 7 +services on ports 5433 and 6380. GitHub-hosted macOS cannot run nested Docker +virtualization, so iOS provisions ephemeral native PostgreSQL 14.x and Redis +7.4.7 on those same ports and starts `pnpm --dir dev-env start:external`. + +Each platform uploads a `nightly-e2e--` artifact for 14 days. +It contains JUnit at `report.xml`, Maestro screenshots, videos, command metadata +and `maestro.log` under `maestro/`, plus Metro, native build, mock-server, service, +dependency, and translation logs. The workflow always uploads what was captured, +including when setup or the native build fails before Maestro starts. + +Add the repository secret `E2E_FAILURES_SLACK_WEBHOOK` before enabling the +schedule. The aggregation job runs even when either platform fails and posts one +detailed Slack notification containing both job statuses, failed flow details or +the failed setup phase, the commit and workflow links, and links to both artifact +sets. Successful runs do not post to Slack. + +Before relying on the schedule, manually dispatch the workflow and verify both +platforms against live Metro and `dev-env`, Android localhost routing, artifact +uploads on success and failure, one Slack message for a forced failure, and no +Slack message for an all-green run. + ## Using Flashlight for Performance Testing 1. Make sure Maestro is installed (optional: only for automated testing) by following the instructions above 2. Install Flashlight by following [these instructions](https://docs.flashlight.dev/) diff --git a/eas.json b/eas.json index 7ab73ab4c8..5bf4e25513 100644 --- a/eas.json +++ b/eas.json @@ -21,6 +21,17 @@ "EXPO_PUBLIC_ENV": "production" } }, + "e2e": { + "extends": "development", + "android": { + "buildType": "apk" + }, + "env": { + "EXPO_PUBLIC_ENV": "e2e", + "NODE_ENV": "test", + "RN_SRC_EXT": "e2e.ts,e2e.tsx" + } + }, "preview": { "extends": "base", "distribution": "internal", diff --git a/package.json b/package.json index 3122d2d7bb..18ed060987 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,7 @@ "expo": "54.0.34", "expo-age-range": "0.2.18", "expo-application": "~7.0.8", + "expo-asset": "~12.0.13", "expo-blur": "~15.0.8", "expo-build-properties": "~1.0.10", "expo-camera": "~17.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 563652a1fb..687c378a94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -436,6 +436,9 @@ importers: expo-application: specifier: ~7.0.8 version: 7.0.8(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + expo-asset: + specifier: ~12.0.13 + version: 12.0.13(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-blur: specifier: ~15.0.8 version: 15.0.8(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx index 7aaa69c47d..4fa17ecf37 100644 --- a/src/lib/media/picker.e2e.tsx +++ b/src/lib/media/picker.e2e.tsx @@ -1,3 +1,4 @@ +import {Asset} from 'expo-asset' import { documentDirectory, getInfoAsync, @@ -9,10 +10,15 @@ import ExpoImageCropTool, { } from '@bsky.app/expo-image-crop-tool' import {IMAGE_SIZE_CONFIG_2K_1MB} from '#/lib/constants' +import {IS_ANDROID} from '#/env' import {compressIfNeeded} from './manip' import {type PickerImage} from './picker.shared' async function getFile() { + if (IS_ANDROID) { + return await getAndroidFile() + } + const imagesDir = documentDirectory! .split('/') .slice(0, -6) @@ -41,6 +47,44 @@ async function getFile() { ) } +/* + * The Android emulator can't reach the iOS simulator's sample photo library, + * so we load a jpg bundled with the app instead. It is bundled via require() + * (resolved by Metro), so it survives `pm clear`, which Maestro's clearState + * runs at the start of every flow. An adb-seeded file in app-scoped external + * storage does not survive: pm clear wipes that directory each flow, so the + * seeded file is gone before the picker mock ever reads it. + */ +async function getAndroidFile() { + const asset = Asset.fromModule( + require('../../../assets/images/welcome-modal-bg.jpg'), + ) + await asset.downloadAsync() + + const path = asset.localUri! + const fileInfo = await getInfoAsync(path) + + if (!fileInfo.exists) { + throw new Error('Failed to get file info') + } + + /* + * Dimensions of the bundled asset (assets/images/welcome-modal-bg.jpg). Only + * used for downstream aspect-ratio display; the actual bytes are read from + * disk by compressIfNeeded. + */ + return await compressIfNeeded( + { + path, + mime: 'image/jpeg', + size: fileInfo.size, + width: 1432, + height: 1025, + }, + IMAGE_SIZE_CONFIG_2K_1MB, + ) +} + export async function openPicker(): Promise { return [await getFile()] } diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx index 2110c9540e..f3f7743b7e 100644 --- a/src/view/com/testing/TestCtrls.e2e.tsx +++ b/src/view/com/testing/TestCtrls.e2e.tsx @@ -18,12 +18,25 @@ LogBox.ignoreAllLogs() const BTN = {height: 1, width: 1, backgroundColor: 'red'} +/* + * This component is mounted inside in + * App.tsx, so it fully remounts whenever the account changes (sign-in / + * sign-out). If the "proxy configured" flag lived only in React state it would + * reset to false on every remount, hiding the sign-in buttons. Keeping it at + * module level lets it survive remounts so the sign-in buttons stay visible + * across sign-out during multi-account flows. Module state still resets when + * the app relaunches with cleared state at the start of each flow, which is the + * desired gating behavior. + */ +let hasConfiguredProxy = false + export function TestCtrls() { const agent = useAgent() const queryClient = useQueryClient() const {logoutEveryAccount, login} = useSessionApi() const onboardingDispatch = useOnboardingDispatch() const {setShowLoggedOut} = useLoggedOutViewControls() + const [isProxyConfigured, setIsProxyConfigured] = useState(hasConfiguredProxy) const onPressSignInAlice = async () => { console.info('[E2E] Signing in as Alice') await login( @@ -63,21 +76,27 @@ export function TestCtrls() { const header = `${proxyHeader}#bsky_appview` BLUESKY_PROXY_HEADER.set(header) agent.configureProxy(header as any) + hasConfiguredProxy = true + setIsProxyConfigured(true) }} style={BTN} /> - - + {isProxyConfigured && ( + <> + + + + )} logoutEveryAccount('Settings')} From 1803de03a4cb2cb0b757f960a57b475bf9bda55b Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:29:58 -0700 Subject: [PATCH 04/32] Lighten blue (link) text for dim and dark themes (#11164) --- oxlint-suppressions.json | 8 ------ package.json | 2 +- pnpm-lock.yaml | 10 +++---- src/components/Composer/index.tsx | 2 +- src/components/FeedInterstitials.tsx | 4 +-- src/components/Link.tsx | 10 +++---- src/components/Post/ShowMoreTextButton.tsx | 12 ++++----- src/components/Post/Translated/index.tsx | 8 +++--- src/components/moderation/ContentHider.tsx | 27 +++++++------------ src/components/moderation/ScreenHider.tsx | 16 +++++------ .../Settings/components/SettingsList.tsx | 2 +- .../com/composer/text-input/TextInput.tsx | 13 ++++----- src/view/com/posts/ViewFullThread.tsx | 3 ++- 13 files changed, 49 insertions(+), 68 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index bf1ed59cf4..f533d7cb0d 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1617,14 +1617,6 @@ "count": 1 } }, - "src/view/com/composer/text-input/TextInput.tsx": { - "typescript/no-floating-promises": { - "count": 1 - }, - "typescript/no-misused-promises": { - "count": 1 - } - }, "src/view/com/composer/text-input/TextInput.web.tsx": { "typescript/no-misused-promises": { "count": 1 diff --git a/package.json b/package.json index 18ed060987..39804cb8e1 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "@atproto/syntax": "0.7.2", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", - "@bsky.app/alf": "^0.1.14", + "@bsky.app/alf": "^0.1.15", "@bsky.app/expo-dynamic-app-icon": "^1.8.5", "@bsky.app/expo-guess-language": "^0.2.8", "@bsky.app/expo-image-crop-tool": "^0.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 687c378a94..b0fb87565e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,8 +257,8 @@ importers: specifier: ^6.0.2 version: 6.0.4 '@bsky.app/alf': - specifier: ^0.1.14 - version: 0.1.14(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + specifier: ^0.1.15 + version: 0.1.15(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) '@bsky.app/expo-dynamic-app-icon': specifier: ^1.8.5 version: 1.8.5(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -1606,8 +1606,8 @@ packages: '@braintree/sanitize-url@6.0.4': resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} - '@bsky.app/alf@0.1.14': - resolution: {integrity: sha512-c/KK6avyjEnYzhXDsN0rQaTFSbi8BPg4sJCW8aUk8AtKsO34GTaY6FU1adQ2i8xVfe5UIRu9FJjnBoGpuXj21A==} + '@bsky.app/alf@0.1.15': + resolution: {integrity: sha512-e6blt+oZ2klv+Cp7u11FeZckoILdXOtUZ11ATERgjZgb4zX3y7rt4e2fI03JkJO31wTvXHG8A1LnM96Az+ndJg==} peerDependencies: react: '*' react-native: '*' @@ -10072,7 +10072,7 @@ snapshots: '@braintree/sanitize-url@6.0.4': {} - '@bsky.app/alf@0.1.14(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/alf@0.1.15(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': dependencies: react: 19.1.0 react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index d93c7bb91a..f4a0302ff7 100644 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -286,7 +286,7 @@ export function Composer({ ref={IS_WEB ? sift.refs.setAnchor : undefined} style={ node.type === 'facet' && { - color: t.palette.primary_500, + color: t.atoms.text_link.color, } }> {node.raw} diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 02dee2fd6e..7f8820f36f 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -481,11 +481,11 @@ export function ProfileGrid({ See more diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 0818b2a26f..6d60edb615 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -415,7 +415,7 @@ function LinkPeek({ // dialog can show. useInAppBrowser: useInAppBrowserPref === true, browserToolbarColor: t.atoms.bg.backgroundColor, - browserControlsColor: t.palette.primary_500, + browserControlsColor: t.atoms.text_link.color, }} borderRadius={borderRadius} // Fires only when not morphing natively (in-app browser off/unset). @@ -487,14 +487,14 @@ export function InlineLinkText({ accessibilityLabel={label} {...rest} style={[ - {color: t.palette.primary_500}, + t.atoms.text_link, interacted && !disableUnderline && { ...web({ outline: 0, textDecorationLine: 'underline', textDecorationColor: - flattenedStyle.color ?? t.palette.primary_500, + flattenedStyle.color ?? t.atoms.text_link.color, }), }, flattenedStyle, @@ -591,14 +591,14 @@ export function SimpleInlineLinkText({ accessibilityLabel={label} {...rest} style={[ - {color: t.palette.primary_500}, + t.atoms.text_link, interacted && !disableUnderline && { ...web({ outline: 0, textDecorationLine: 'underline', textDecorationColor: - flattenedStyle.color ?? t.palette.primary_500, + flattenedStyle.color ?? t.atoms.text_link.color, }), }, flattenedStyle, diff --git a/src/components/Post/ShowMoreTextButton.tsx b/src/components/Post/ShowMoreTextButton.tsx index 1e4e13cd5d..7765fbdd07 100644 --- a/src/components/Post/ShowMoreTextButton.tsx +++ b/src/components/Post/ShowMoreTextButton.tsx @@ -1,8 +1,6 @@ import {useCallback, useMemo} from 'react' import {LayoutAnimation, type TextStyle} from 'react-native' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {HITSLOP_10} from '#/lib/constants' import {atoms as a, flatten, type TextStyleProp, useTheme} from '#/alf' @@ -14,7 +12,7 @@ export function ShowMoreTextButton({ style, }: TextStyleProp & {onPress: () => void}) { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() const onPress = useCallback(() => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) @@ -30,7 +28,7 @@ export function ShowMoreTextButton({ return ( diff --git a/src/components/Post/Translated/index.tsx b/src/components/Post/Translated/index.tsx index 9aff552bbf..94e0ea697a 100644 --- a/src/components/Post/Translated/index.tsx +++ b/src/components/Post/Translated/index.tsx @@ -150,10 +150,10 @@ function TranslationLink({ label={l`Translate`} hoverStyle={[ native({opacity: 0.5}), - web([a.underline, {textDecorationColor: t.palette.primary_500}]), + web([a.underline, {textDecorationColor: t.atoms.text_link.color}]), ]} hitSlop={HITSLOP_30}> - + Translate @@ -229,7 +229,7 @@ function TranslationError({ label={l`Try Google Translate`} hoverStyle={[ native({opacity: 0.5}), - web([a.underline, {textDecorationColor: t.palette.primary_500}]), + web([a.underline, {textDecorationColor: t.atoms.text_link.color}]), ]} hitSlop={HITSLOP_30}> Try Google Translate diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx index b603c738d4..cfdc6c7e12 100644 --- a/src/components/moderation/ContentHider.tsx +++ b/src/components/moderation/ContentHider.tsx @@ -6,9 +6,7 @@ import { type ViewStyle, } from 'react-native' import {type ModerationUI} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import { ADULT_CONTENT_LABELS, @@ -78,7 +76,7 @@ function ContentHiderActive({ children?: React.ReactNode }) { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() const {gtMobile} = useBreakpoints() const [override, setOverride] = useState(false) const control = useModerationDetailsDialogControl() @@ -97,7 +95,7 @@ function ContentHiderActive({ (blur.type === 'label' && blur.source.type !== 'user') ) { if (desc.isSubjectAccount) { - return _(msg`${desc.name} (Account)`) + return l`${desc.name} (Account)` } else { return desc.name } @@ -128,7 +126,7 @@ function ContentHiderActive({ const def = cause.labelDef || getDefinition(labelDefs, cause.label) if (def.identifier === 'porn' || def.identifier === 'sexual') { - return _(msg`Adult Content`) + return l`Adult Content` } return getLabelStrings(i18n.locale, globalLabelStrings, def).name }) @@ -138,7 +136,7 @@ function ContentHiderActive({ } return [...new Set(selfBlurNames)].join(', ') }, [ - _, + l, modui.blurs, blur, desc.name, @@ -151,7 +149,6 @@ function ContentHiderActive({ return ( - - {desc.source && blur.type === 'label' && !override && ( )} - {override && {children}} ) diff --git a/src/components/moderation/ScreenHider.tsx b/src/components/moderation/ScreenHider.tsx index cb3d522274..3c0d4584b8 100644 --- a/src/components/moderation/ScreenHider.tsx +++ b/src/components/moderation/ScreenHider.tsx @@ -6,9 +6,7 @@ import { type ViewStyle, } from 'react-native' import {type ModerationUI} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' @@ -38,7 +36,7 @@ export function ScreenHider({ containerStyle?: StyleProp }>) { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() const [override, setOverride] = useState(false) const navigation = useNavigation() const {isMobile} = useWebMediaQueries() @@ -131,15 +129,13 @@ export function ScreenHider({ control.open() }} accessibilityRole="button" - accessibilityLabel={_(msg`Learn more about this warning`)} + accessibilityLabel={l`Learn more about this warning`} accessibilityHint=""> { if (navigation.canGoBack()) { navigation.goBack() @@ -176,7 +172,7 @@ export function ScreenHider({ color="secondary" size="large" style={[a.rounded_full]} - label={_(msg`Show anyway`)} + label={l`Show anyway`} onPress={() => setOverride(v => !v)}> Show anyway diff --git a/src/screens/Settings/components/SettingsList.tsx b/src/screens/Settings/components/SettingsList.tsx index b23eab85f4..268e49339f 100644 --- a/src/screens/Settings/components/SettingsList.tsx +++ b/src/screens/Settings/components/SettingsList.tsx @@ -311,7 +311,7 @@ export function BadgeButton({ a.text_md, a.font_normal, a.text_right, - {color: pressed ? t.palette.contrast_300 : t.palette.primary_500}, + {color: pressed ? t.palette.contrast_300 : t.atoms.text_link.color}, ]}> {label} diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 313f0becbf..d40e06a215 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -6,10 +6,9 @@ import { useState, } from 'react' import { - type NativeSyntheticEvent, Text as RNText, TextInput as RNTextInput, - type TextInputSelectionChangeEventData, + type TextInputSelectionChangeEvent, View, } from 'react-native' import {type PasteEventPayload, TextInputWrapper} from 'expo-paste-input' @@ -141,7 +140,7 @@ export function TextInput({ ) const onSelectionChange = useCallback( - (evt: NativeSyntheticEvent) => { + (evt: TextInputSelectionChangeEvent) => { // NOTE we track the input selection using a ref to avoid excessive renders -prf textInputSelection.current = evt.nativeEvent.selection }, @@ -150,7 +149,7 @@ export function TextInput({ const onSelectAutocompleteItem = useCallback( (item: string) => { - onChangeText( + void onChangeText( insertMentionAt( richtext.text, textInputSelection.current?.start || 0, @@ -201,7 +200,9 @@ export function TextInput({ style={[ inputTextStyle, { - color: segment.facet ? t.palette.primary_500 : t.atoms.text.color, + color: segment.facet + ? t.atoms.text_link.color + : t.atoms.text.color, marginTop: -1, }, ]}> @@ -217,7 +218,7 @@ export function TextInput({ void onChangeText(newText)} onSelectionChange={onSelectionChange} placeholder={placeholder} placeholderTextColor={t.atoms.text_contrast_low.color} diff --git a/src/view/com/posts/ViewFullThread.tsx b/src/view/com/posts/ViewFullThread.tsx index 2a0eb5135f..f346a34396 100644 --- a/src/view/com/posts/ViewFullThread.tsx +++ b/src/view/com/posts/ViewFullThread.tsx @@ -58,7 +58,8 @@ export function ViewFullThread({uri}: {uri: string}) { {/* HACKFIX: Trans isn't working after SDK 53 upgrade -sfn */} {l`View full thread`} From 92ef1528bf4d982980b3bd16b3f90a9b32f217d4 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 20 Jul 2026 19:44:17 +0300 Subject: [PATCH 05/32] Stop closing search suggestions on input blur (#11199) --- src/screens/Search/Shell.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/screens/Search/Shell.tsx b/src/screens/Search/Shell.tsx index 951d415a87..681ee25273 100644 --- a/src/screens/Search/Shell.tsx +++ b/src/screens/Search/Shell.tsx @@ -474,17 +474,6 @@ export function SearchScreenShell({ } }, [setShowAutocomplete]) - const onSearchInputBlur = useCallback(() => { - /* - * Bind autocomplete visibility to focus state on native. On web this - * doesn't work because of focus management, which would render the - * autocomplete results uninteractable. - */ - if (IS_NATIVE) { - setShowAutocomplete(false) - } - }, []) - const focusSearchInput = useCallback( (tab?: TabParam) => { textInput.current?.focus() @@ -609,7 +598,6 @@ export function SearchScreenShell({ ref={textInput} value={searchText} onFocus={onSearchInputFocus} - onBlur={onSearchInputBlur} onChangeText={onChangeText} onClearText={onPressClearQuery} onSubmitEditing={onSubmit('typed')} From 0f27fdda654dcdfdc63d39d00c4fcfcc2e96219f Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:01:31 -0700 Subject: [PATCH 06/32] Add height constraint to full-height bottom sheets (#11178) --- modules/bottom-sheet/src/BottomSheetNativeComponent.tsx | 3 ++- src/components/Dialog/index.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx index 0c3c700e90..f2955395e6 100644 --- a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx @@ -146,6 +146,7 @@ function BottomSheetNativeComponentInner({ const insets = useSafeAreaInsets() const cornerRadius = rest.cornerRadius ?? 0 const {height: screenHeight} = useWindowDimensions() + const isHeightConstrained = maxHeight != null || rest.fullHeight === true // sigh... on older Android versions, screenHeight does not include safe area insets // on newer Androids + iOS, it does. we need to find the inner bit + the bottom inset @@ -182,7 +183,7 @@ function BottomSheetNativeComponentInner({ ]}> + style={isHeightConstrained ? {flex: 1} : undefined}> {children} diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index bd23e566ad..c9f23175de 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -157,7 +157,8 @@ export function Outer({ [open, close], ) - const isHeightConstrained = nativeOptions?.maxHeight != null + const isHeightConstrained = + nativeOptions?.maxHeight != null || nativeOptions?.fullHeight === true const context = useMemo( () => ({ From d3e7d99fa7d7e7702a700e091d77beced6788692 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 20 Jul 2026 16:47:05 -0500 Subject: [PATCH 07/32] Fix image crop in quote posts, remove `PostEmbedViewContext.FeedEmbedRecordWithMedia` (#11205) --- src/components/Post/Embed/ImageEmbed.tsx | 5 ++--- src/components/Post/Embed/types.ts | 1 - src/components/images/ImageLayoutGrid.tsx | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index ecc0c09d13..9eea748a33 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -115,6 +115,7 @@ export function ImageEmbed({ onPress(0, [singleContainerRef.current], [singleDimsRef.current]) } } + console.log(rest.viewContext, 'rest.viewContext') return ( { diff --git a/src/components/Post/Embed/types.ts b/src/components/Post/Embed/types.ts index 77319b0e86..0ef5569c7d 100644 --- a/src/components/Post/Embed/types.ts +++ b/src/components/Post/Embed/types.ts @@ -4,7 +4,6 @@ import {type AppBskyFeedDefs, type ModerationDecision} from '@atproto/api' export enum PostEmbedViewContext { ThreadHighlighted = 'ThreadHighlighted', Feed = 'Feed', - FeedEmbedRecordWithMedia = 'FeedEmbedRecordWithMedia', ChatMessage = 'ChatMessage', } diff --git a/src/components/images/ImageLayoutGrid.tsx b/src/components/images/ImageLayoutGrid.tsx index 5b4ba608b2..f69de63bd1 100644 --- a/src/components/images/ImageLayoutGrid.tsx +++ b/src/components/images/ImageLayoutGrid.tsx @@ -5,7 +5,7 @@ import {type AppBskyEmbedImages} from '@atproto/api' import {atoms as a, useBreakpoints} from '#/alf' import {type Dimensions} from '#/components/Lightbox/types' -import {PostEmbedViewContext} from '#/components/Post/Embed/types' +import {type PostEmbedViewContext} from '#/components/Post/Embed/types' import {GalleryItem} from './ImageLayoutGridItem' interface ImageLayoutGridProps { @@ -28,9 +28,7 @@ export function ImageLayoutGrid({ ...props }: ImageLayoutGridProps) { const {gtMobile} = useBreakpoints() - const isWithinQuote = - isWithinQuoteProp ?? - props.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia + const isWithinQuote = isWithinQuoteProp const gap = isWithinQuote ? (gtMobile ? a.gap_xs : a.gap_2xs) : a.gap_xs return ( From baaf2bdc2b25da5d4ae9e309dc8eb40c56ba3703 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 20 Jul 2026 17:06:14 -0500 Subject: [PATCH 08/32] Don't roll up account labels into additional post alerts (#11204) --- src/components/moderation/PostAlerts.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/moderation/PostAlerts.tsx b/src/components/moderation/PostAlerts.tsx index 4379c76cb3..af8329bec5 100644 --- a/src/components/moderation/PostAlerts.tsx +++ b/src/components/moderation/PostAlerts.tsx @@ -54,7 +54,15 @@ export function PostAlerts({ const isOwnPost = !!post && post.author.did === currentAccount?.did const allLabels: ComAtprotoLabelDefs.Label[] = isOwnPost && view === 'expanded' - ? [...(post.labels ?? []), ...(post.author.labels ?? [])] + ? [ + ...(post.labels ?? []), + /* + * Account labels appear on Profile. We don't show them here unless the + * user's mod settings are configured such that the labels land in the + * modui handling. + */ + // ...(post.author.labels ?? []) + ] : [] /* * Labels that the moderation system already surfaces in this context - From daa11b63e719780a16c9ecf399e9bdf1d25c18d0 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Tue, 21 Jul 2026 03:10:47 +0000 Subject: [PATCH 09/32] Nightly source-language update --- src/locale/locales/en/messages.po | 88 +++++++++++++++---------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index cee2f855de..e87e5bf40b 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -98,12 +98,12 @@ msgid "{0, plural, one {# hour} other {# hours}}" msgstr "" #. placeholder {0}: labels.length -#: src/components/moderation/PostAlerts.tsx:149 +#: src/components/moderation/PostAlerts.tsx:157 msgid "{0, plural, one {# label applied to your post} other {# labels applied to your post}}" msgstr "{0, plural, one {# label applied to your post} other {# labels applied to your post}}" #. placeholder {0}: labels.length -#: src/components/moderation/PostAlerts.tsx:156 +#: src/components/moderation/PostAlerts.tsx:164 msgid "{0, plural, one {# label applied} other {# labels applied}}" msgstr "{0, plural, one {# label applied} other {# labels applied}}" @@ -219,7 +219,7 @@ msgid "{0} Β· {1}" msgstr "" #. placeholder {0}: desc.name -#: src/components/moderation/ContentHider.tsx:100 +#: src/components/moderation/ContentHider.tsx:98 msgid "{0} (Account)" msgstr "" @@ -837,7 +837,7 @@ msgstr "" #. Number of images beyond the first 3 #. placeholder {0}: labels.length #. placeholder {0}: totalNumber - 3 -#: src/components/moderation/PostAlerts.tsx:155 +#: src/components/moderation/PostAlerts.tsx:163 #: src/view/com/composer/ComposerReplyTo.tsx:278 msgid "+{0}" msgstr "+{0}" @@ -1395,7 +1395,7 @@ msgstr "" msgid "Adult content" msgstr "" -#: src/components/moderation/ContentHider.tsx:131 +#: src/components/moderation/ContentHider.tsx:129 #: src/lib/moderation/useGlobalLabelStrings.ts:34 #: src/lib/moderation/useModerationCauseDescription.ts:149 #: src/view/com/composer/labels/LabelsBtn.tsx:123 @@ -2580,7 +2580,7 @@ msgstr "" msgid "Cancel reply" msgstr "Cancel reply" -#: src/screens/Search/Shell.tsx:595 +#: src/screens/Search/Shell.tsx:584 msgid "Cancel search" msgstr "" @@ -3278,7 +3278,7 @@ msgid "Content promoting or depicting self-harm" msgstr "" #: src/components/moderation/ModerationDetailsDialog.tsx:70 -#: src/components/moderation/ScreenHider.tsx:100 +#: src/components/moderation/ScreenHider.tsx:98 #: src/lib/moderation/useGlobalLabelStrings.ts:22 #: src/lib/moderation/useModerationCauseDescription.ts:46 msgid "Content Warning" @@ -3363,7 +3363,7 @@ msgstr "Conversation not found." msgid "Copied build version to clipboard" msgstr "" -#: src/screens/Search/Shell.tsx:515 +#: src/screens/Search/Shell.tsx:504 msgid "Copied link to clipboard" msgstr "Copied link to clipboard" @@ -4089,7 +4089,7 @@ msgstr "" msgid "Discover New Feeds" msgstr "" -#: src/components/Dialog/index.tsx:413 +#: src/components/Dialog/index.tsx:414 #: src/features/inviteFriends/components/FollowersPromoBanner.tsx:83 msgid "Dismiss" msgstr "" @@ -4215,7 +4215,7 @@ msgstr "" msgid "Double tap or long press the message to add a reaction" msgstr "" -#: src/components/Dialog/index.tsx:414 +#: src/components/Dialog/index.tsx:415 msgid "Double tap to close the dialog" msgstr "" @@ -4750,7 +4750,7 @@ msgstr "" msgid "Expand or collapse the full post you are replying to" msgstr "" -#: src/components/Post/ShowMoreTextButton.tsx:33 +#: src/components/Post/ShowMoreTextButton.tsx:31 msgid "Expand post text" msgstr "" @@ -4798,7 +4798,7 @@ msgid "Explicit sexual images." msgstr "" #: src/Navigation.tsx:760 -#: src/screens/Search/Shell.tsx:552 +#: src/screens/Search/Shell.tsx:541 #: src/view/shell/desktop/LeftNav.tsx:677 #: src/view/shell/Drawer.tsx:473 msgid "Explore" @@ -4881,7 +4881,7 @@ msgstr "" #: src/components/Lightbox/Lightbox.web.tsx:302 #: src/features/inviteFriends/InviteFriendsDialogInner.tsx:130 -#: src/screens/Search/Shell.tsx:516 +#: src/screens/Search/Shell.tsx:505 msgid "Failed to copy link" msgstr "Failed to copy link" @@ -5369,7 +5369,7 @@ msgstr "" msgid "Find people you know" msgstr "Find people you know" -#: src/screens/Search/Shell.tsx:765 +#: src/screens/Search/Shell.tsx:753 msgid "Find posts, users, and feeds on Bluesky" msgstr "" @@ -5808,9 +5808,9 @@ msgstr "" #: src/components/dialogs/LinkWarning.tsx:127 #: src/components/dialogs/LinkWarning.tsx:133 -#: src/components/Layout/Header/index.tsx:133 -#: src/components/moderation/ScreenHider.tsx:161 -#: src/components/moderation/ScreenHider.tsx:170 +#: src/components/Layout/Header/index.tsx:134 +#: src/components/moderation/ScreenHider.tsx:157 +#: src/components/moderation/ScreenHider.tsx:166 #: src/screens/Login/components/AuthLayout/Header/index.tsx:74 #: src/screens/Login/components/ConfirmHostingProviderDialog.tsx:160 #: src/screens/Login/components/ConfirmHostingProviderDialog.tsx:163 @@ -6145,7 +6145,7 @@ msgstr "" #: src/components/interstitials/Trending.tsx:133 #: src/components/interstitials/TrendingVideos.tsx:139 -#: src/components/moderation/ContentHider.tsx:220 +#: src/components/moderation/ContentHider.tsx:217 #: src/components/moderation/LabelPreference.tsx:141 #: src/components/moderation/PostHider.tsx:140 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:820 @@ -6235,7 +6235,7 @@ msgstr "" msgid "Hide verification badges" msgstr "" -#: src/components/moderation/ContentHider.tsx:171 +#: src/components/moderation/ContentHider.tsx:168 #: src/components/moderation/PostHider.tsx:94 msgid "Hides the content" msgstr "" @@ -6826,11 +6826,11 @@ msgid "KWS website" msgstr "" #. placeholder {0}: sanitizeDisplayName(desc.source!) -#: src/components/moderation/ContentHider.tsx:251 +#: src/components/moderation/ContentHider.tsx:245 msgid "Labeled by {0}." msgstr "" -#: src/components/moderation/ContentHider.tsx:249 +#: src/components/moderation/ContentHider.tsx:243 msgid "Labeled by the author." msgstr "" @@ -6899,7 +6899,7 @@ msgctxt "english-only-resource" msgid "Learn more" msgstr "" -#: src/components/moderation/ScreenHider.tsx:147 +#: src/components/moderation/ScreenHider.tsx:143 msgid "Learn More" msgstr "" @@ -6932,8 +6932,8 @@ msgstr "" msgid "Learn more about self hosting your PDS." msgstr "" -#: src/components/moderation/ContentHider.tsx:169 -#: src/components/moderation/ContentHider.tsx:235 +#: src/components/moderation/ContentHider.tsx:166 +#: src/components/moderation/ContentHider.tsx:230 msgid "Learn more about the moderation applied to this content" msgstr "" @@ -6946,7 +6946,7 @@ msgid "Learn more about these changes and how to share your thoughts with us by msgstr "" #: src/components/moderation/PostHider.tsx:116 -#: src/components/moderation/ScreenHider.tsx:134 +#: src/components/moderation/ScreenHider.tsx:132 msgid "Learn more about this warning" msgstr "" @@ -6970,7 +6970,7 @@ msgid "Learn more in your <0>account settings." msgstr "" #: src/components/dialogs/ServerInput.tsx:222 -#: src/components/moderation/ContentHider.tsx:259 +#: src/components/moderation/ContentHider.tsx:253 #: src/screens/Login/components/HostingProviderDialog.tsx:243 msgid "Learn more." msgstr "" @@ -8565,7 +8565,7 @@ msgstr "" msgid "Open draft" msgstr "" -#: src/components/Layout/Header/index.tsx:167 +#: src/components/Layout/Header/index.tsx:168 msgid "Open drawer menu" msgstr "" @@ -10555,9 +10555,9 @@ msgstr "" #: src/components/forms/SearchInput.tsx:53 #: src/screens/Search/components/AdvancedSearchDialog/index.tsx:218 #: src/screens/Search/components/AdvancedSearchDialog/index.tsx:224 -#: src/screens/Search/Shell.tsx:552 -#: src/screens/Search/Shell.tsx:616 -#: src/screens/Search/Shell.tsx:753 +#: src/screens/Search/Shell.tsx:541 +#: src/screens/Search/Shell.tsx:604 +#: src/screens/Search/Shell.tsx:741 #: src/view/shell/bottom-bar/BottomBar.tsx:216 msgid "Search" msgstr "" @@ -11164,8 +11164,8 @@ msgstr "" msgid "Share this feed" msgstr "" -#: src/screens/Search/Shell.tsx:568 -#: src/screens/Search/Shell.tsx:638 +#: src/screens/Search/Shell.tsx:557 +#: src/screens/Search/Shell.tsx:626 msgid "Share this search" msgstr "Share this search" @@ -11210,7 +11210,7 @@ msgstr "Sharing your age range reveals only the range associated with your Apple msgid "Sharing your age range reveals only the range associated with your Google Account – for example, that you’re at least 18. <0>Your exact age and birthday are never shared, and this data never leaves this device. Therefore, it only enables access on this device. Alternatively, <1>you can use our trusted partner, KWS, to complete your verification and enable access on all platforms." msgstr "Sharing your age range reveals only the range associated with your Google Account – for example, that you’re at least 18. <0>Your exact age and birthday are never shared, and this data never leaves this device. Therefore, it only enables access on this device. Alternatively, <1>you can use our trusted partner, KWS, to complete your verification and enable access on all platforms." -#: src/components/moderation/ContentHider.tsx:220 +#: src/components/moderation/ContentHider.tsx:217 #: src/components/moderation/LabelPreference.tsx:143 #: src/components/moderation/PostHider.tsx:140 msgid "Show" @@ -11220,8 +11220,8 @@ msgstr "" msgid "Show alt text" msgstr "" -#: src/components/moderation/ScreenHider.tsx:179 -#: src/components/moderation/ScreenHider.tsx:182 +#: src/components/moderation/ScreenHider.tsx:175 +#: src/components/moderation/ScreenHider.tsx:178 #: src/features/liveNow/components/LiveStatusDialog.tsx:318 #: src/features/liveNow/components/LiveStatusDialog.tsx:322 #: src/screens/List/ListHiddenScreen.tsx:194 @@ -11265,9 +11265,9 @@ msgstr "" msgid "Show lists of users to select from" msgstr "" -#: src/components/Post/ShowMoreTextButton.tsx:52 -msgid "Show More" -msgstr "" +#: src/components/Post/ShowMoreTextButton.tsx:50 +msgid "Show more" +msgstr "Show more" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:594 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:596 @@ -11332,7 +11332,7 @@ msgstr "" msgid "Shows other accounts you can switch to" msgstr "" -#: src/components/moderation/ContentHider.tsx:172 +#: src/components/moderation/ContentHider.tsx:169 #: src/components/moderation/PostHider.tsx:94 msgid "Shows the content" msgstr "" @@ -11427,7 +11427,7 @@ msgstr "" msgid "Sign up" msgstr "Sign up" -#: src/components/moderation/ScreenHider.tsx:98 +#: src/components/moderation/ScreenHider.tsx:96 #: src/lib/moderation/useGlobalLabelStrings.ts:28 msgid "Sign-in Required" msgstr "" @@ -12383,7 +12383,7 @@ msgstr "They own this chat" msgid "They won’t be able to rejoin unless you invite them again." msgstr "They won’t be able to rejoin unless you invite them again." -#: src/components/moderation/ScreenHider.tsx:118 +#: src/components/moderation/ScreenHider.tsx:116 msgid "This {screenDescription} has been flagged:" msgstr "" @@ -12399,7 +12399,7 @@ msgstr "This account has been marked as automated by its owner." msgid "This account has one or more attempted verifications, but it is not currently verified." msgstr "" -#: src/components/moderation/ScreenHider.tsx:113 +#: src/components/moderation/ScreenHider.tsx:111 msgid "This account has requested that users sign in to view their profile." msgstr "" @@ -13249,7 +13249,7 @@ msgstr "" msgid "Unsubscribed from list" msgstr "" -#: src/view/com/composer/text-input/TextInput.tsx:128 +#: src/view/com/composer/text-input/TextInput.tsx:127 msgid "Unsupported clipboard content" msgstr "Unsupported clipboard content" @@ -13721,7 +13721,7 @@ msgid "View details" msgstr "" #: src/view/com/posts/ViewFullThread.tsx:31 -#: src/view/com/posts/ViewFullThread.tsx:64 +#: src/view/com/posts/ViewFullThread.tsx:65 msgid "View full thread" msgstr "" From 0ba78623d644856ce1d75fe031ccb0e1a80b1ec9 Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Tue, 21 Jul 2026 08:07:43 -0400 Subject: [PATCH 10/32] Fix image menu in landscape orientation (#11188) --- src/components/Lightbox/chrome/ImageMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Lightbox/chrome/ImageMenu.tsx b/src/components/Lightbox/chrome/ImageMenu.tsx index 92b5c47749..632f5eb362 100644 --- a/src/components/Lightbox/chrome/ImageMenu.tsx +++ b/src/components/Lightbox/chrome/ImageMenu.tsx @@ -78,6 +78,7 @@ export function ImageMenu({onPressShare, onPressSave}: Props) { visible={isMounted} animationType="none" onRequestClose={close} + supportedOrientations={['portrait', 'landscape']} statusBarTranslucent> Date: Tue, 21 Jul 2026 05:08:41 -0700 Subject: [PATCH 11/32] Bump actions/setup-node from 6.4.0 to 7.0.0 (#11203) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bundle-deploy-eas-update.yml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/nightly-update-source-languages.yaml | 2 +- .github/workflows/pull-request-comment.yml | 2 +- .github/workflows/pull-request-commit.yml | 6 +++--- .github/workflows/verify-pnpm-lock.yml | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml index 58fcdaf6f9..eab0851c5b 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -86,7 +86,7 @@ jobs: - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: πŸ”§ Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 49bc67ada9..a3f43dd99c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -53,7 +53,7 @@ jobs: exit $rc - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Install node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm @@ -91,7 +91,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Install node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm diff --git a/.github/workflows/nightly-update-source-languages.yaml b/.github/workflows/nightly-update-source-languages.yaml index 52e0f2c2c7..32ba5e811f 100644 --- a/.github/workflows/nightly-update-source-languages.yaml +++ b/.github/workflows/nightly-update-source-languages.yaml @@ -21,7 +21,7 @@ jobs: ssh-key: ${{secrets.GH_ACTION_DEPLOY_KEY}} - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Install node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm diff --git a/.github/workflows/pull-request-comment.yml b/.github/workflows/pull-request-comment.yml index 46b169e541..944801326a 100644 --- a/.github/workflows/pull-request-comment.yml +++ b/.github/workflows/pull-request-comment.yml @@ -132,7 +132,7 @@ jobs: - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: πŸ”§ Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm diff --git a/.github/workflows/pull-request-commit.yml b/.github/workflows/pull-request-commit.yml index 5f568dde78..264ee5d311 100644 --- a/.github/workflows/pull-request-commit.yml +++ b/.github/workflows/pull-request-commit.yml @@ -32,7 +32,7 @@ jobs: - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: πŸ”§ Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm @@ -73,7 +73,7 @@ jobs: - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: πŸ”§ Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm @@ -168,7 +168,7 @@ jobs: - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: πŸ”§ Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json cache: pnpm diff --git a/.github/workflows/verify-pnpm-lock.yml b/.github/workflows/verify-pnpm-lock.yml index 8483e5fdb0..407ca6a345 100644 --- a/.github/workflows/verify-pnpm-lock.yml +++ b/.github/workflows/verify-pnpm-lock.yml @@ -27,7 +27,7 @@ jobs: - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Install node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: package.json From 7d4b91dd38cc925388a78ff8052d66db945ef37f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 05:09:01 -0700 Subject: [PATCH 12/32] Bump the actions group with 4 updates (#11202) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-submit-android.yml | 10 +++++----- .github/workflows/build-submit-ios.yml | 2 +- .github/workflows/claude-mention.yml | 4 ++-- .github/workflows/claude-review.yml | 4 ++-- .github/workflows/nightly-build.yml | 4 ++-- .github/workflows/nightly-e2e.yml | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index e4a86fc224..a2261536e6 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -88,7 +88,7 @@ jobs: with: expo-token: ${{ secrets.EXPO_TOKEN }} - - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + - uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0 with: distribution: "temurin" java-version: "17" @@ -162,7 +162,7 @@ jobs: - name: πŸ”” Notify Slack of Play Store Submission if: ${{ inputs.profile == 'production' }} - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} webhook-type: incoming-webhook @@ -201,7 +201,7 @@ jobs: # bundletool needs a JRE. ubuntu-latest ships a default JDK, but pin it explicitly # like the build job so the toolchain is deterministic. - - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + - uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0 with: distribution: "temurin" java-version: "17" @@ -251,7 +251,7 @@ jobs: path: build.apk - name: πŸ”” Notify Slack of APK Artifact - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} webhook-type: incoming-webhook @@ -314,7 +314,7 @@ jobs: - name: πŸ”” Notify Slack of Release Attachment if: ${{ steps.release-check.outputs.exists == 'true' }} - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} webhook-type: incoming-webhook diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml index 5c26977f23..2085aabaa4 100644 --- a/.github/workflows/build-submit-ios.yml +++ b/.github/workflows/build-submit-ios.yml @@ -246,7 +246,7 @@ jobs: - name: πŸ”” Notify Slack of Production Build if: ${{ inputs.profile == 'production' }} - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} webhook-type: incoming-webhook diff --git a/.github/workflows/claude-mention.yml b/.github/workflows/claude-mention.yml index 4c4b6236f0..3e22d05f6c 100644 --- a/.github/workflows/claude-mention.yml +++ b/.github/workflows/claude-mention.yml @@ -59,13 +59,13 @@ jobs: fetch-depth: 1 - name: Configure AWS credentials (OIDC) - uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 + uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2 with: role-to-assume: ${{ secrets.AWS_BEDROCK_REVIEW_ROLE_ARN }} aws-region: us-east-2 - name: Claude - uses: anthropics/claude-code-action@f87768c6d25f92ae6efa7175e223ef77d4cbf97f # v1.0.166 + uses: anthropics/claude-code-action@e90deca47693f9457b72f2b53c17d7c445a87342 # v1.0.171 with: use_bedrock: 'true' additional_permissions: | diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index a66147a6e3..35587011c6 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -45,13 +45,13 @@ jobs: fetch-depth: 1 - name: Configure AWS credentials (OIDC) - uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1 + uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2 with: role-to-assume: ${{ secrets.AWS_BEDROCK_REVIEW_ROLE_ARN }} aws-region: us-east-2 - name: Claude review - uses: anthropics/claude-code-action@f87768c6d25f92ae6efa7175e223ef77d4cbf97f # v1.0.166 + uses: anthropics/claude-code-action@e90deca47693f9457b72f2b53c17d7c445a87342 # v1.0.171 with: use_bedrock: 'true' additional_permissions: | diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index a90eb55fe5..53fc480e89 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -144,7 +144,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: πŸ”” Notify Slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }} webhook-type: incoming-webhook @@ -174,7 +174,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: πŸ”” Notify Slack - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }} webhook-type: incoming-webhook diff --git a/.github/workflows/nightly-e2e.yml b/.github/workflows/nightly-e2e.yml index 8b06e36764..1b2a5282a5 100644 --- a/.github/workflows/nightly-e2e.yml +++ b/.github/workflows/nightly-e2e.yml @@ -51,7 +51,7 @@ jobs: expo-token: ${{ secrets.EXPO_TOKEN }} - name: Set up Java 17 - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0 with: distribution: temurin java-version: "17" @@ -176,7 +176,7 @@ jobs: expo-token: ${{ secrets.EXPO_TOKEN }} - name: Set up Java 17 - uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0 with: distribution: temurin java-version: "17" @@ -417,7 +417,7 @@ jobs: - name: Notify Slack of E2E failures if: steps.summary.outputs.notify == 'true' - uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.E2E_FAILURES_SLACK_WEBHOOK }} webhook-type: incoming-webhook From 92ec563f9dba16fd040e791170458f3f93f8a31f Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 21 Jul 2026 05:16:22 -0700 Subject: [PATCH 13/32] Restore activity notification subscriptions screen (#11201) --- bskyweb/cmd/bskyweb/server.go | 1 + src/Navigation.tsx | 9 + src/lib/routes/types.ts | 1 + src/routes.ts | 1 + .../ActivityNotificationSettings.tsx | 264 ++++++++++++++++++ .../Settings/NotificationSettings/index.tsx | 20 +- 6 files changed, 279 insertions(+), 17 deletions(-) create mode 100644 src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 7373dc5cdc..eb59adf40f 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -328,6 +328,7 @@ func serve(cctx *cli.Context) error { e.GET("/settings/interests", server.WebGenericNoindex) e.GET("/settings/about", server.WebGenericNoindex) e.GET("/settings/notifications", server.WebGenericNoindex) + e.GET("/settings/notifications/activity", server.WebGenericNoindex) e.GET("/sys/debug", server.WebGenericNoindex) e.GET("/sys/debug-mod", server.WebGenericNoindex) e.GET("/sys/log", server.WebGenericNoindex) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 5207a3a123..ab596876aa 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -117,6 +117,7 @@ import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings' import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' +import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings' import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' import {SettingsScreen} from '#/screens/Settings/Settings' import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' @@ -451,6 +452,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { getComponent={() => NotificationSettingsScreen} options={{title: title(msg`Notification settings`), requireAuth: true}} /> + ActivityNotificationSettingsScreen} + options={{ + title: title(msg`Activity notifications`), + requireAuth: true, + }} + /> ContentAndMediaSettingsScreen} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 6eb112a381..9108b01e07 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -70,6 +70,7 @@ export type CommonNavigatorParams = { ActivityPrivacySettings: undefined ContentAndMediaSettings: undefined NotificationSettings: undefined + ActivityNotificationSettings: undefined InterestsSettings: undefined AboutSettings: undefined AppIconSettings: undefined diff --git a/src/routes.ts b/src/routes.ts index 3e45f1b379..c996dfde9b 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -61,6 +61,7 @@ export const router = new Router({ AboutSettings: '/settings/about', AppIconSettings: '/settings/app-icon', NotificationSettings: '/settings/notifications', + ActivityNotificationSettings: '/settings/notifications/activity', FindContactsSettings: '/settings/find-contacts', // support Support: '/support', diff --git a/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx new file mode 100644 index 0000000000..168593ebe8 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx @@ -0,0 +1,264 @@ +import {useCallback, useMemo} from 'react' +import {type ListRenderItemInfo, Text as RNText, View} from 'react-native' +import {type ModerationOpts} from '@atproto/api' +import {useLingui} from '@lingui/react/macro' +import {Trans} from '@lingui/react/macro' + +import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {cleanError} from '#/lib/strings/errors' +import {logger} from '#/logger' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useActivitySubscriptionsQuery} from '#/state/queries/activity-subscriptions' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {List} from '#/view/com/util/List' +import {atoms as a, useTheme} from '#/alf' +import {SubscribeProfileDialog} from '#/components/activity-notifications/SubscribeProfileDialog' +import * as Admonition from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import { + BellRinging_Filled_Corner0_Rounded as BellRingingFilledIcon, + BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon, +} from '#/components/icons/BellRinging' +import * as Layout from '#/components/Layout' +import {InlineLinkText} from '#/components/Link' +import {ListFooter} from '#/components/Lists' +import {Loader} from '#/components/Loader' +import * as ProfileCard from '#/components/ProfileCard' +import {Text} from '#/components/Typography' +import type * as bsky from '#/types/bsky' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'ActivityNotificationSettings' +> + +export function ActivityNotificationSettingsScreen({}: Props) { + const t = useTheme() + const {t: l} = useLingui() + const {data: preferences, isError: isPreferencesError} = + useNotificationSettingsQuery() + const moderationOpts = useModerationOpts() + + const { + data: subscriptions, + isPending, + isError: isSubscriptionsError, + error, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + } = useActivitySubscriptionsQuery() + + const items = useMemo(() => { + if (!subscriptions) return [] + return subscriptions.pages.flatMap(page => page.subscriptions) + }, [subscriptions]) + + const renderItem = useCallback( + ({item}: ListRenderItemInfo) => { + if (!moderationOpts) return null + return ( + + ) + }, + [moderationOpts], + ) + + const onEndReached = useCallback(() => { + if (isFetchingNextPage || !hasNextPage || isSubscriptionsError) return + void fetchNextPage().catch(err => { + logger.error('Failed to load more activity subscriptions', { + message: err, + }) + }) + }, [isFetchingNextPage, hasNextPage, isSubscriptionsError, fetchNextPage]) + + return ( + + + + + + Notifications + + + + + + + + + + {isPreferencesError ? ( + + + Failed to load notification settings. + + + ) : ( + + + + )} + + } + data={items} + keyExtractor={keyExtractor} + renderItem={renderItem} + onEndReached={onEndReached} + onEndReachedThreshold={4} + ListEmptyComponent={ + error ? null : ( + + {!isPending ? ( + + + + + + + Enable notifications for an account by visiting their + profile and pressing the{' '} + + bell icon + {' '} + + . + + + + + If you want to restrict who can receive notifications + for your account's activity, you can change this in{' '} + + Settings → Privacy and Security + + . + + + + + + ) : ( + + + + )} + + ) + } + ListFooterComponent={ + + } + windowSize={11} + /> + + ) +} + +function keyExtractor(item: bsky.profile.AnyProfileView) { + return item.did +} + +function ActivitySubscriptionCard({ + profile: profileUnshadowed, + moderationOpts, +}: { + profile: bsky.profile.AnyProfileView + moderationOpts: ModerationOpts +}) { + const profile = useProfileShadow(profileUnshadowed) + const control = useDialogControl() + const {t: l} = useLingui() + const t = useTheme() + + const preview = useMemo(() => { + const actSub = profile.viewer?.activitySubscription + if (actSub?.post && actSub?.reply) { + return l`Posts, Replies` + } else if (actSub?.post) { + return l`Posts` + } else if (actSub?.reply) { + return l`Replies` + } + return l`None` + }, [l, profile.viewer?.activitySubscription]) + + return ( + + + + + + + + {preview} + + + + + + + + ) +} diff --git a/src/screens/Settings/NotificationSettings/index.tsx b/src/screens/Settings/NotificationSettings/index.tsx index 00318a9180..16671508b2 100644 --- a/src/screens/Settings/NotificationSettings/index.tsx +++ b/src/screens/Settings/NotificationSettings/index.tsx @@ -57,7 +57,6 @@ export function NotificationSettingsScreen({}: Props) { const mentionDialogControl = Dialog.useDialogControl() const quoteDialogControl = Dialog.useDialogControl() const repostDialogControl = Dialog.useDialogControl() - const activityDialogControl = Dialog.useDialogControl() const likeRepostDialogControl = Dialog.useDialogControl() const repostRepostDialogControl = Dialog.useDialogControl() const chatDialogControl = Dialog.useDialogControl() @@ -217,9 +216,9 @@ export function NotificationSettingsScreen({}: Props) { showSkeleton={!settings} /> - - + Get notifications when people repost your posts. } /> - Activity from others} - subtitleText={ - - Get notifications when there's activity on posts you're subscribed - to. - - } - allowDisableInApp={false} - /> Date: Tue, 21 Jul 2026 15:21:51 +0300 Subject: [PATCH 14/32] TypeScript 7 (attempt 2) (#11169) --- package.json | 10 +- pnpm-lock.yaml | 260 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 204 insertions(+), 66 deletions(-) diff --git a/package.json b/package.json index 39804cb8e1..ae08413d33 100644 --- a/package.json +++ b/package.json @@ -63,9 +63,9 @@ "lint-native": "swiftlint ./modules && ktlint ./modules", "lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules", "typecheck": "pnpm run typecheck:ios && pnpm run typecheck:android && pnpm run typecheck:web", - "typecheck:ios": "tsgo --project ./tsconfig.check.ios.json", - "typecheck:android": "tsgo --project ./tsconfig.check.android.json", - "typecheck:web": "tsgo --project ./tsconfig.check.web.json", + "typecheck:ios": "tsc --project ./tsconfig.check.ios.json", + "typecheck:android": "tsc --project ./tsconfig.check.android.json", + "typecheck:web": "tsc --project ./tsconfig.check.web.json", "e2e:mock-server": "cd dev-env && pnpm start", "e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", "e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android", @@ -274,7 +274,7 @@ "@types/psl": "1.1.1", "@types/react": "^19.1.17", "@types/react-dom": "^19.1.11", - "@typescript/native-preview": "^7.0.0-dev.20260428.1", + "@typescript/native": "npm:typescript@^7.0.2", "babel-jest": "^29.7.0", "babel-plugin-module-resolver": "^5.0.2", "babel-plugin-react-compiler": "19.1.0-rc.3", @@ -299,7 +299,7 @@ "react-refresh": "^0.14.0", "svgo": "^4.0.2", "ts-plugin-sort-import-suggestions": "^1.0.4", - "typescript": "^6.0.2", + "typescript": "npm:@typescript/typescript6@^6.0.2", "webpack-bundle-analyzer": "^4.10.1" }, "jest": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0fb87565e..b64d2b20cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -330,10 +330,10 @@ importers: version: 9.2.7 '@lingui/core': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)) '@lingui/react': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))(react@19.1.0) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))(react@19.1.0) '@react-native-async-storage/async-storage': specifier: 2.2.0 version: 2.2.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)) @@ -730,10 +730,10 @@ importers: version: 4.14.2 '@lingui/babel-plugin-lingui-macro': specifier: ^5.9.2 - version: 5.9.5(typescript@6.0.3) + version: 5.9.5(@typescript/typescript6@6.0.2) '@lingui/cli': specifier: ^5.9.2 - version: 5.9.5(typescript@6.0.3) + version: 5.9.5(@typescript/typescript6@6.0.2) '@pmmmwh/react-refresh-webpack-plugin': specifier: ^0.5.15 version: 0.5.17(react-refresh@0.14.2)(type-fest@1.4.0)(webpack-dev-server@4.15.2(webpack@5.106.2(postcss@8.5.14)))(webpack@5.106.2(postcss@8.5.14)) @@ -770,9 +770,9 @@ importers: '@types/react-dom': specifier: ^19.1.11 version: 19.1.11(@types/react@19.1.17) - '@typescript/native-preview': - specifier: ^7.0.0-dev.20260428.1 - version: 7.0.0-dev.20260512.1 + '@typescript/native': + specifier: npm:typescript@^7.0.2 + version: typescript@7.0.2 babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.29.0) @@ -849,8 +849,8 @@ importers: specifier: ^1.0.4 version: 1.0.4 typescript: - specifier: ^6.0.2 - version: 6.0.3 + specifier: npm:@typescript/typescript6@^6.0.2 + version: '@typescript/typescript6@6.0.2' webpack-bundle-analyzer: specifier: ^4.10.1 version: 4.10.2 @@ -3718,51 +3718,128 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-l9AJi/TIVMPx5R1c7fxZCSA7eUaHeA0C9Mxdxx/oQJo1K/GtbI3mzYe/SiKNltko1KSdKUmWVhPwxTOS289REg==} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-oABZLQrfB8JN2Ct2CiLK5PyE28Em3sIJlZsAMD45/A2ymtIaa5826dwv8vapE5Wjp54ao0LXxCSuKFm1A8zzCQ==} + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} engines: {node: '>=16.20.0'} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-xvbwzpTe+5N6bnBI/t9n4zsGzXxz3V6rVbvDUoJmRLfav5fz+ck0QDkGQGUPrQEEIp0KEzQvx7c+AEZnzdvTQA==} + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-0Hs1Gqa/t9cthoPdqHud1pFGUr9DgJivBTjwquTUh8jt/6PI2bQxoMNZLiN/bhqeDFDTzdxoMBfCaytsTMcXqw==} + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} engines: {node: '>=16.20.0'} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-qr5h6FPo74bN/U+EwRuayBhUbxaji8xzFbIbhMOA2oYSc/qozp5ia2g1+9xGw67MXxPPw/IPT+UGvrNK7K1NeQ==} + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} engines: {node: '>=16.20.0'} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-meNWxhNEfaqos2U0JXvfxWvy4JWrKE9fZepCndDZi+t04X+AIiLYp5s6crWnKP67nzVAzMNgTAc8mu8CnGM+/A==} + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} engines: {node: '>=16.20.0'} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-Hp6vBnxJSKEEAVWgIoWMmfqkZXCdkhm6XTivrwgRzBwWfiTVe2ZyZ7byWegIKeNnBbffg/K2KvoM8JAHl059GQ==} + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} engines: {node: '>=16.20.0'} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260512.1': - resolution: {integrity: sha512-KIzYPGuxZnyiiYkYrozDT94Af2nwbdLXoY1cgGY66RRa9HSEw13RH9WHg8wA8fZhT4wYzF5uF7WY3hz0QhaxGg==} - engines: {node: '>=16.20.0'} + '@typescript/typescript6@6.0.2': + resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} hasBin: true '@ungap/structured-clone@1.3.1': @@ -8658,6 +8735,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + ua-parser-js@0.7.41: resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==} hasBin: true @@ -10905,19 +10987,19 @@ snapshots: '@lingui/babel-plugin-extract-messages@5.9.5': {} - '@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)': + '@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)': dependencies: '@babel/core': 7.29.0 '@babel/runtime': 7.29.2 '@babel/types': 7.29.0 - '@lingui/conf': 5.9.5(typescript@6.0.3) - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) + '@lingui/conf': 5.9.5(@typescript/typescript6@6.0.2) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)) '@lingui/message-utils': 5.9.5 transitivePeerDependencies: - supports-color - typescript - '@lingui/cli@5.9.5(typescript@6.0.3)': + '@lingui/cli@5.9.5(@typescript/typescript6@6.0.2)': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -10925,10 +11007,10 @@ snapshots: '@babel/runtime': 7.29.2 '@babel/types': 7.29.0 '@lingui/babel-plugin-extract-messages': 5.9.5 - '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3) - '@lingui/conf': 5.9.5(typescript@6.0.3) - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) - '@lingui/format-po': 5.9.5(typescript@6.0.3) + '@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2) + '@lingui/conf': 5.9.5(@typescript/typescript6@6.0.2) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)) + '@lingui/format-po': 5.9.5(@typescript/typescript6@6.0.2) '@lingui/message-utils': 5.9.5 chokidar: 3.5.1 cli-table: 0.3.11 @@ -10951,26 +11033,26 @@ snapshots: - supports-color - typescript - '@lingui/conf@5.9.5(typescript@6.0.3)': + '@lingui/conf@5.9.5(@typescript/typescript6@6.0.2)': dependencies: '@babel/runtime': 7.29.2 - cosmiconfig: 8.3.6(typescript@6.0.3) + cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) jest-validate: 29.7.0 jiti: 2.7.0 picocolors: 1.1.1 transitivePeerDependencies: - typescript - '@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))': + '@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))': dependencies: '@babel/runtime': 7.29.2 '@lingui/message-utils': 5.9.5 optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3) + '@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2) - '@lingui/format-po@5.9.5(typescript@6.0.3)': + '@lingui/format-po@5.9.5(@typescript/typescript6@6.0.2)': dependencies: - '@lingui/conf': 5.9.5(typescript@6.0.3) + '@lingui/conf': 5.9.5(@typescript/typescript6@6.0.2) '@lingui/message-utils': 5.9.5 date-fns: 3.6.0 pofile: 1.1.4 @@ -10982,13 +11064,13 @@ snapshots: '@messageformat/parser': 5.1.1 js-sha256: 0.10.1 - '@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3))(react@19.1.0)': + '@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))(react@19.1.0)': dependencies: '@babel/runtime': 7.29.2 - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(typescript@6.0.3)) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)) react: 19.1.0 optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(typescript@6.0.3) + '@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2) '@messageformat/parser@5.1.1': dependencies: @@ -12566,36 +12648,69 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260512.1': + '@typescript/typescript-aix-ppc64@7.0.2': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260512.1': + '@typescript/typescript-darwin-arm64@7.0.2': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260512.1': + '@typescript/typescript-darwin-x64@7.0.2': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260512.1': + '@typescript/typescript-freebsd-arm64@7.0.2': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260512.1': + '@typescript/typescript-freebsd-x64@7.0.2': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260512.1': + '@typescript/typescript-linux-arm64@7.0.2': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260512.1': + '@typescript/typescript-linux-arm@7.0.2': optional: true - '@typescript/native-preview@7.0.0-dev.20260512.1': - optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260512.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260512.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260512.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260512.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260512.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260512.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260512.1 + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@typescript/typescript6@6.0.2': + dependencies: + '@typescript/old': typescript@6.0.3 '@ungap/structured-clone@1.3.1': {} @@ -13437,14 +13552,14 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig@8.3.6(typescript@6.0.3): + cosmiconfig@8.3.6(@typescript/typescript6@6.0.2): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 6.0.3 + typescript: '@typescript/typescript6@6.0.2' create-jest@29.7.0(@types/node@24.12.4): dependencies: @@ -18113,6 +18228,29 @@ snapshots: typescript@6.0.3: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + ua-parser-js@0.7.41: {} ua-parser-js@1.0.41: {} From 26143c1a48b96f2bc788638dfd6bfcabb85601fa Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Tue, 21 Jul 2026 11:32:53 -0400 Subject: [PATCH 15/32] add client event for failed video playback (#11170) --- src/analytics/metrics/types.ts | 20 ++++++++ .../VideoEmbedInnerWeb.native.tsx | 1 + .../VideoEmbedInnerWeb.shared.ts | 13 +++++ .../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 4 +- .../Post/Embed/VideoEmbed/index.tsx | 9 ++++ .../Post/Embed/VideoEmbed/index.web.tsx | 48 +++++++++++++++++-- src/screens/VideoFeed/index.tsx | 21 +++++++- 7 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index ef875ff5bf..89c6632589 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -1346,6 +1346,26 @@ export type Events = { // user dismissed the empty-followers promo banner 'invite:followersPromo:dismiss': {} + /** + * Fired when a video fails terminally during playback: unreachable (404), + * undecodable, or the client lacks the required codecs. Complements the + * Sentry-only video.playback spans with a countable, unsampled event. + */ + 'video:playback:failed': { + surface: 'feed' | 'immersiveFeed' + presentation: 'video' | 'gif' + /** + * Coarse failure bucket: VideoNotFoundError, HLSUnsupportedError, an + * hls.js error details code (e.g. bufferAppendError), or PlayerError on + * native. + */ + errorClass: string + /** Truncated to 256 chars */ + errorMessage: string + /** HLS playlist URL, identifies the exact video for server-side lookup */ + playlist: string + } + // === Video upload funnel (Frontend Spec section D) === // Every event carries uploadId (client-generated UUID, ties one upload // session end-to-end) + engine (compression engine id, e.g. diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx index 10e8c73236..75756ac35c 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx @@ -1,6 +1,7 @@ import {type VideoEmbedInnerWebProps} from './VideoEmbedInnerWeb.shared' export { + HLSFatalError, HLSUnsupportedError, VideoNotFoundError, } from './VideoEmbedInnerWeb.shared' diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts index 06abf40408..d7c44b0d91 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts @@ -19,3 +19,16 @@ export class VideoNotFoundError extends Error { super('Video not found') } } + +/** + * Fatal hls.js playback error. `detail` is the hls.js error details code + * (e.g. bufferAppendError), which buckets failures more usefully than the + * error message. + */ +export class HLSFatalError extends Error { + detail: string + constructor(detail: string, cause: Error) { + super(cause.message, {cause}) + this.detail = detail + } +} diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx index 72d9f3a8ba..6ece8f5a9b 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -10,6 +10,7 @@ import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog' import {useFullscreen} from '#/components/hooks/useFullscreen' import * as BandwidthEstimate from './bandwidth-estimate' import { + HLSFatalError, HLSUnsupportedError, type VideoEmbedInnerWebProps, VideoNotFoundError, @@ -17,6 +18,7 @@ import { import {Controls} from './web-controls/VideoControls' export { + HLSFatalError, HLSUnsupportedError, VideoNotFoundError, } from './VideoEmbedInnerWeb.shared' @@ -306,7 +308,7 @@ function useHLS({ ) { setError(new VideoNotFoundError()) } else { - setError(data.error) + setError(new HLSFatalError(data.details, data.error)) } } else { console.error(data.error) diff --git a/src/components/Post/Embed/VideoEmbed/index.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx index 66693e780c..d78aad4985 100644 --- a/src/components/Post/Embed/VideoEmbed/index.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.tsx @@ -16,6 +16,7 @@ import {Button} from '#/components/Button' import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {ConstrainedImage} from '#/components/images/AutoSizedImage' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' +import {useAnalytics} from '#/analytics' import {GifPresentationControls} from './GifPresentationControls' import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative' import * as VideoFallback from './VideoEmbedInner/VideoFallback' @@ -70,6 +71,7 @@ export function VideoEmbed({embed}: Props) { function InnerWrapper({embed}: Props) { const {_} = useLingui() + const ax = useAnalytics() const ref = useRef<{togglePlayback: () => void}>(null) const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>( @@ -130,6 +132,13 @@ function InnerWrapper({embed}: Props) { }} onError={error => { telemetryRef.current?.error(error) + ax.metric('video:playback:failed', { + surface: 'feed', + presentation: embed.presentation === 'gif' ? 'gif' : 'video', + errorClass: 'PlayerError', + errorMessage: error.slice(0, 256), + playlist: embed.playlist, + }) }} ref={ref} /> diff --git a/src/components/Post/Embed/VideoEmbed/index.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx index f37ae0d664..8d5a816021 100644 --- a/src/components/Post/Embed/VideoEmbed/index.web.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx @@ -18,10 +18,12 @@ import {useFullscreen} from '#/components/hooks/useFullscreen' import {ConstrainedImage} from '#/components/images/AutoSizedImage' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import { + HLSFatalError, HLSUnsupportedError, VideoEmbedInnerWeb, VideoNotFoundError, } from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb' +import {useAnalytics} from '#/analytics' import {IS_WEB_FIREFOX} from '#/env' import {useActiveVideoWeb} from './ActiveVideoWebContext' import * as VideoFallback from './VideoEmbedInner/VideoFallback' @@ -69,9 +71,9 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { const [key, setKey] = useState(0) const renderError = useCallback( (error: unknown) => ( - setKey(key + 1)} /> + setKey(key + 1)} /> ), - [key], + [key, embed], ) let aspectRatio: number | undefined @@ -222,23 +224,63 @@ export const OnlyNearScreen = ({children}: {children: React.ReactNode}) => { return nearScreen ? children : null } -function VideoError({error, retry}: {error: unknown; retry: () => void}) { +function VideoError({ + embed, + error, + retry, +}: { + embed: AppBskyEmbedVideo.View + error: unknown + retry: () => void +}) { const {_} = useLingui() + const ax = useAnalytics() let showRetryButton = true let text = null + let errorClass: string if (error instanceof VideoNotFoundError) { text = _(msg`Video not found.`) + errorClass = 'VideoNotFoundError' } else if (error instanceof HLSUnsupportedError) { showRetryButton = false text = _( msg`This video can’t be played on your device. Your browser or system may be missing the required video codecs (H.264/AAC).`, ) + errorClass = 'HLSUnsupportedError' } else { text = _(msg`An error occurred while loading the video. Please try again.`) + if (error instanceof HLSFatalError) { + errorClass = error.detail + } else if (error instanceof Error) { + errorClass = error.name || 'Error' + } else { + errorClass = 'Unknown' + } } + const errorMessage = error instanceof Error ? error.message : String(error) + const presentation = embed.presentation === 'gif' ? 'gif' : 'video' + const playlist = embed.playlist + /* + * Fire exactly once per failure - the analytics context identity can change + * (session/geolocation updates) while this fallback stays mounted, which + * would otherwise re-run the effect and double-count. + */ + const fired = useRef(false) + useEffect(() => { + if (fired.current) return + fired.current = true + ax.metric('video:playback:failed', { + surface: 'feed', + presentation, + errorClass, + errorMessage: errorMessage.slice(0, 256), + playlist, + }) + }, [ax, presentation, playlist, errorClass, errorMessage]) + return ( {text} diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx index 99dc03d208..2949c6a777 100644 --- a/src/screens/VideoFeed/index.tsx +++ b/src/screens/VideoFeed/index.tsx @@ -588,7 +588,7 @@ function VideoItemInner({ const {bottom} = useSafeAreaInsets() const [isReady, setIsReady] = useState(!IS_ANDROID) - usePlaybackTelemetry({player, active}) + usePlaybackTelemetry({player, active, playlist: embed.playlist}) useEventListener(player, 'timeUpdate', evt => { if (IS_ANDROID && !isReady && evt.currentTime >= 0.05) { @@ -625,10 +625,13 @@ function VideoItemInner({ function usePlaybackTelemetry({ player, active, + playlist, }: { player: VideoPlayer active: boolean + playlist: string }) { + const ax = useAnalytics() const telemetryRef = useRef(null) useEffect(() => { @@ -652,7 +655,21 @@ function usePlaybackTelemetry({ if (evt.status === 'readyToPlay') { telemetryRef.current?.ready() } else if (evt.status === 'error') { - telemetryRef.current?.error(evt.error?.message ?? 'unknown') + const message = evt.error?.message ?? 'unknown' + telemetryRef.current?.error(message) + /* + * Adjacent players are preloaded and can error before the user ever + * swipes to them - only count failures the user actually sees. + */ + if (active) { + ax.metric('video:playback:failed', { + surface: 'immersiveFeed', + presentation: 'video', + errorClass: 'PlayerError', + errorMessage: message.slice(0, 256), + playlist, + }) + } } }) From 86f0bedfbeb065d6be589bdb64b31cdeac904263 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:18:12 -0700 Subject: [PATCH 16/32] Add new search results tab for starter packs (#11226) --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/analytics/metrics/types.ts | 6 +- src/screens/Search/SearchResults.tsx | 133 +++++++++++++++++- .../Search/components/StarterPackCard.tsx | 12 +- src/state/queries/starter-pack-search.ts | 75 ++++++++++ 6 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 src/state/queries/starter-pack-search.ts diff --git a/package.json b/package.json index ae08413d33..51289561e4 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "prettier": "prettier --check ." }, "dependencies": { - "@atproto/api": "0.20.28", + "@atproto/api": "0.20.31", "@atproto/common-web": "0.5.6", "@atproto/syntax": "0.7.2", "@bitdrift/react-native": "^0.6.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b64d2b20cb..1da8629683 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,8 +242,8 @@ importers: .: dependencies: '@atproto/api': - specifier: 0.20.28 - version: 0.20.28 + specifier: 0.20.31 + version: 0.20.31 '@atproto/common-web': specifier: 0.5.6 version: 0.5.6 @@ -865,8 +865,8 @@ packages: graphql: optional: true - '@atproto/api@0.20.28': - resolution: {integrity: sha512-/Rvk8zt9mtRi9tlMD2Qg+NG2lMj3B0HDjmfswR5724pH7GsOEMDDHwleVlmOBSrgACIyVSa5tdCDJ+R+SEhwww==} + '@atproto/api@0.20.31': + resolution: {integrity: sha512-TovCQLQv5ti1jqh8UH6jJ0EFuWRjGdUtFyFR5xYC/IkIulwHDuyrVaXdCv7VLWiHftp92DevtZnFRm+BZsZZdw==} engines: {node: '>=22'} '@atproto/common-web@0.5.6': @@ -9208,7 +9208,7 @@ snapshots: '@0no-co/graphql.web@1.2.0': {} - '@atproto/api@0.20.28': + '@atproto/api@0.20.31': dependencies: '@atproto/common-web': 0.5.6 '@atproto/lexicon': 0.7.7 diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 89c6632589..aa525e6495 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -779,13 +779,13 @@ export type Events = { } 'search:results:loaded': { - tab: 'top' | 'latest' | 'people' | 'feeds' + tab: 'top' | 'latest' | 'people' | 'feeds' | 'starterPacks' initialCount: number } 'search:result:press': { - tab?: 'top' | 'latest' | 'people' | 'feeds' - resultType: 'post' | 'profile' | 'feed' + tab?: 'top' | 'latest' | 'people' | 'feeds' | 'starterPacks' + resultType: 'post' | 'profile' | 'feed' | 'starterPack' position: number uri: string } diff --git a/src/screens/Search/SearchResults.tsx b/src/screens/Search/SearchResults.tsx index 70755e8148..ed405d3265 100644 --- a/src/screens/Search/SearchResults.tsx +++ b/src/screens/Search/SearchResults.tsx @@ -1,6 +1,6 @@ import {memo, useCallback, useMemo, useState} from 'react' import {ActivityIndicator, View} from 'react-native' -import {type AppBskyFeedDefs} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyGraphDefs} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {urls} from '#/lib/constants' @@ -15,6 +15,7 @@ import {augmentSearchQuery} from '#/lib/strings/helpers' import {useActorSearch} from '#/state/queries/actor-search' import {usePopularFeedsSearch} from '#/state/queries/feed' import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2' +import {useStarterPackSearch} from '#/state/queries/starter-pack-search' import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' @@ -23,6 +24,7 @@ import {TabBar} from '#/view/com/pager/TabBar' import {Post} from '#/view/com/post/Post' import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' import {List} from '#/view/com/util/List' +import {StarterPackCard} from '#/screens/Search/components/StarterPackCard' import { hasPostOnlyFilters, type SearchFilters, @@ -106,6 +108,15 @@ let SearchResults = ({ ), }, + noFilters && { + title: l`Starter packs`, + component: ( + + ), + }, ].filter(Boolean) as { title: string component: React.ReactNode @@ -692,3 +703,123 @@ function SearchFeedCard({ return } + +let SearchScreenStarterPackResults = ({ + query, + active, +}: { + query: string + active: boolean +}): React.ReactNode => { + const ax = useAnalytics() + const {t: l} = useLingui() + const [isPTR, setIsPTR] = useState(false) + + const { + isFetched, + data: results, + isFetching, + error, + refetch, + fetchNextPage, + isFetchingNextPage, + hasNextPage, + } = useStarterPackSearch({ + query, + enabled: active, + }) + + const onPullToRefresh = useCallback(async () => { + setIsPTR(true) + await refetch() + setIsPTR(false) + }, [setIsPTR, refetch]) + const onEndReached = useCallback(() => { + if (isFetching || !hasNextPage || error) return + void fetchNextPage() + }, [isFetching, error, hasNextPage, fetchNextPage]) + const starterPacks = useMemo(() => { + return results?.pages.flatMap(page => page.starterPacks) || [] + }, [results]) + + const fireTracking = useCallOnce(() => { + ax.metric('search:results:loaded', { + tab: 'starterPacks', + initialCount: starterPacks.length, + }) + }) + if (isFetched) { + fireTracking() + } + + if (error) { + return ( + + ) + } + + return isFetched ? ( + <> + {starterPacks.length ? ( + ( + + + + )} + keyExtractor={(item: AppBskyGraphDefs.StarterPackView) => item.uri} + refreshing={isPTR} + onRefresh={() => void onPullToRefresh()} + onEndReached={onEndReached} + desktopFixedHeight + ListFooterComponent={ + + } + /> + ) : ( + } /> + )} + + ) : ( + + ) +} +SearchScreenStarterPackResults = memo(SearchScreenStarterPackResults) + +function SearchStarterPack({ + position, + view, +}: { + position: number + view: AppBskyGraphDefs.StarterPackView +}) { + const ax = useAnalytics() + + const handleOnPress = () => { + ax.metric('search:result:press', { + tab: 'starterPacks', + resultType: 'starterPack', + position, + uri: view.uri, + }) + } + + return +} diff --git a/src/screens/Search/components/StarterPackCard.tsx b/src/screens/Search/components/StarterPackCard.tsx index bc0920fdb8..9831746b42 100644 --- a/src/screens/Search/components/StarterPackCard.tsx +++ b/src/screens/Search/components/StarterPackCard.tsx @@ -26,8 +26,10 @@ import * as bsky from '#/types/bsky' export function StarterPackCard({ view, + onPress, }: { view: AppBskyGraphDefs.StarterPackView + onPress?: () => void }) { const t = useTheme() const {_} = useLingui() @@ -55,7 +57,10 @@ export function StarterPackCard({ to={link.to} label={link.label} onHoverIn={link.precache} - onPress={link.precache}> + onPress={() => { + link.precache() + onPress?.() + }}> {s => ( <> @@ -111,7 +116,10 @@ export function StarterPackCard({ to={link.to} label={link.label} onHoverIn={link.precache} - onPress={link.precache} + onPress={() => { + link.precache() + onPress?.() + }} variant="solid" color="secondary" size="small" diff --git a/src/state/queries/starter-pack-search.ts b/src/state/queries/starter-pack-search.ts new file mode 100644 index 0000000000..7987d1e07f --- /dev/null +++ b/src/state/queries/starter-pack-search.ts @@ -0,0 +1,75 @@ +import {type AppBskyGraphSearchStarterPacksV2} from '@atproto/api' +import { + type InfiniteData, + keepPreviousData, + type QueryKey, + useInfiniteQuery, +} from '@tanstack/react-query' + +import {STALE} from '#/state/queries' +import {useAgent} from '#/state/session' + +export const RQKEY_ROOT = 'starter-pack-search' +export const RQKEY = (query: string, limit?: number) => [ + RQKEY_ROOT, + query, + limit, +] + +export function useStarterPackSearch({ + query, + enabled, + maintainData, + limit = 25, +}: { + query: string + enabled?: boolean + maintainData?: boolean + limit?: number +}) { + const agent = useAgent() + return useInfiniteQuery< + AppBskyGraphSearchStarterPacksV2.OutputSchema, + Error, + InfiniteData, + QueryKey, + string | undefined + >({ + staleTime: STALE.MINUTES.FIVE, + queryKey: RQKEY(query, limit), + queryFn: async ({pageParam}) => { + const res = await agent.app.bsky.graph.searchStarterPacksV2({ + q: query, + limit, + cursor: pageParam, + }) + return res.data + }, + enabled: enabled && !!query, + initialPageParam: undefined, + getNextPageParam: lastPage => lastPage.cursor, + placeholderData: maintainData ? keepPreviousData : undefined, + select, + }) +} + +function select( + data: InfiniteData, +) { + // enforce uniqueness + const uris = new Set() + + return { + ...data, + pages: data.pages.map(page => ({ + ...page, + starterPacks: page.starterPacks.filter(starterPack => { + if (uris.has(starterPack.uri)) { + return false + } + uris.add(starterPack.uri) + return true + }), + })), + } +} From cbf0d891281e1d18cd0f0fc15bd2cfba87dc6449 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:25:01 -0700 Subject: [PATCH 17/32] Remove console.log, address lint (#11228) --- oxlint-suppressions.json | 8 -------- src/components/Post/Embed/ImageEmbed.tsx | 8 ++++---- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index f533d7cb0d..4b2bbcadd8 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -244,14 +244,6 @@ "count": 3 } }, - "src/components/Post/Embed/ImageEmbed.tsx": { - "typescript/no-explicit-any": { - "count": 2 - }, - "typescript/no-floating-promises": { - "count": 1 - } - }, "src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx": { "typescript/no-floating-promises": { "count": 2 diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index 9eea748a33..235a82ebaf 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -59,7 +59,7 @@ export function ImageEmbed({ // Captured from AutoSizedImage so the peek-commit handler can reuse the same // ref + dims that a tap would β€” keeps the lightbox's return animation intact. - const singleContainerRef = useRef | null>(null) + const singleContainerRef = useRef | null>(null) const singleDimsRef = useRef(null) if (images.length > 0) { @@ -71,7 +71,7 @@ export function ImageEmbed({ })) const onPress = ( index: number, - refs: AnimatedRef[], + refs: AnimatedRef[], fetchedDims: (Dimensions | null)[], ) => { if (postContext) { @@ -97,7 +97,7 @@ export function ImageEmbed({ } const onPressIn = (_: number) => { InteractionManager.runAfterInteractions(() => { - Image.prefetch( + void Image.prefetch( items.map(i => i.uri), 'memory', ) @@ -115,7 +115,7 @@ export function ImageEmbed({ onPress(0, [singleContainerRef.current], [singleDimsRef.current]) } } - console.log(rest.viewContext, 'rest.viewContext') + return ( Date: Tue, 21 Jul 2026 21:25:15 +0100 Subject: [PATCH 18/32] Squish separate imports from the same source in ActivityNotificationSettings (#11225) --- .../NotificationSettings/ActivityNotificationSettings.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx index 168593ebe8..3aaf5e10e5 100644 --- a/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx +++ b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx @@ -1,8 +1,7 @@ import {useCallback, useMemo} from 'react' import {type ListRenderItemInfo, Text as RNText, View} from 'react-native' import {type ModerationOpts} from '@atproto/api' -import {useLingui} from '@lingui/react/macro' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import { From 44b1ab08b58e9d7da5b5ebe4d7bce4391d247c28 Mon Sep 17 00:00:00 2001 From: Alex Benzer Date: Tue, 21 Jul 2026 13:40:30 -0700 Subject: [PATCH 19/32] Move known likers below stats row; restore original order in stats row (#11192) --- .../PostThread/components/LikesStat.tsx | 151 +++++++----------- .../components/ThreadItemAnchor.tsx | 5 +- 2 files changed, 62 insertions(+), 94 deletions(-) diff --git a/src/screens/PostThread/components/LikesStat.tsx b/src/screens/PostThread/components/LikesStat.tsx index 9a0c7139fb..eab32dc274 100644 --- a/src/screens/PostThread/components/LikesStat.tsx +++ b/src/screens/PostThread/components/LikesStat.tsx @@ -1,15 +1,13 @@ import {View} from 'react-native' import {type AppBskyFeedDefs, AtUri, moderateProfile} from '@atproto/api' -import {plural} from '@lingui/core/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeDisplayName} from '#/lib/strings/display-names' -import {enforceLen} from '#/lib/strings/helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useLikedBySampleQuery} from '#/state/queries/post-liked-by' import {useSession} from '#/state/session' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {AvatarStack} from '#/components/AvatarStack' import {InlineLinkText, Link} from '#/components/Link' import {useFormatPostStatCount} from '#/components/PostControls/util' @@ -18,27 +16,56 @@ import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' const AVI_SIZE = 20 -const MAX_NAME_LENGTH = 16 /** - * The likes stat for the expanded anchor post. When the viewer follows some - * of the post's recent likers, renders social proof - a face pile plus - * "Liked by A, B, and N others" - in place of the plain "N likes" text, - * which it falls back to otherwise. - * - * Known likers are sourced client-side from a single `getLikes` request (100 - * likes, the API max per page), so they are a sample of the most recent - * likers, not an exhaustive list. Only the faces and names are affected by - * sampling - the "N others" count is derived from the post's total like - * count. + * The plain "N likes" stat for the expanded anchor post, linking to the likes + * list. Renders nothing when the post has no likes. */ export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) { const t = useTheme() - const {gtMobile} = useBreakpoints() + const {t: l} = useLingui() + const formatPostStatCount = useFormatPostStatCount() + const ax = useAnalytics() + + const likeCount = post.likeCount ?? 0 + if (likeCount === 0) return null + + const urip = new AtUri(post.uri) + const likesHref = makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') + + return ( + ax.metric('post:likedBy:click', {})}> + + + + {formatPostStatCount(likeCount)} + {' '} + + + + + ) +} + +/** + * Social proof for the expanded anchor post. When the viewer follows some of + * the post's recent likers, renders a face pile plus "Liked by A and B" on + * its own row below the interaction stats line. Renders nothing otherwise. + * + * Known likers are sourced client-side from a single `getLikes` request (100 + * likes, the API max per page), so they are a sample of the most recent + * likers, not an exhaustive list. + */ +export function KnownLikers({post}: {post: AppBskyFeedDefs.PostView}) { + const t = useTheme() const {t: l} = useLingui() const {hasSession, currentAccount} = useSession() const moderationOpts = useModerationOpts() - const formatPostStatCount = useFormatPostStatCount() const ax = useAnalytics() const likeCount = post.likeCount ?? 0 @@ -78,67 +105,34 @@ export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) { knownLikersAndModeration.length > 0 && ax.features.enabled(ax.features.PostThreadKnownLikersEnable) - if (!showKnownLikers) { - return ( - - - - - {formatPostStatCount(likeCount)} - {' '} - - - - - ) - } + if (!showKnownLikers) return null const aviStackProfiles = knownLikersAndModeration .slice(0, 3) .map(({actor}) => actor) - const maxNames = gtMobile ? 2 : 1 const names = knownLikersAndModeration - .slice(0, maxNames) + .slice(0, 2) .map(({actor, moderation}) => { return { did: actor.did, href: makeProfileLink(actor), - displayName: enforceLen( - sanitizeDisplayName( - actor.displayName || actor.handle, - moderation.ui('displayName'), - ), - MAX_NAME_LENGTH, - true, + displayName: sanitizeDisplayName( + actor.displayName || actor.handle, + moderation.ui('displayName'), ), } }) - const others = likeCount - names.length - /* * The row link's a11y label mirrors the visible sentence so screen readers * announce the social proof. */ - const othersLabel = plural(others, { - one: `${formatPostStatCount(others)} other`, - other: `${formatPostStatCount(others)} others`, - }) const rowLabel = names.length >= 2 - ? others > 0 - ? l`${names[0].displayName}, ${names[1].displayName}, and ${othersLabel} like this` - : l`${names[0].displayName} and ${names[1].displayName} like this` - : others > 0 - ? l`${names[0].displayName} and ${othersLabel} like this` - : l`${names[0].displayName} likes this` + ? l`Liked by ${names[0].displayName} and ${names[1].displayName}` + : l`Liked by ${names[0].displayName}` - const textStyle = [a.text_md, t.atoms.text_contrast_medium] - const nameStyle = [a.text_md, a.font_semi_bold, t.atoms.text] + const textStyle = [a.text_sm, t.atoms.text_contrast_medium] + const nameStyle = [a.text_sm, a.font_semi_bold, t.atoms.text] /* * Nested inside the row link, but the deepest link claims the press, so @@ -160,10 +154,8 @@ export function LikesStat({post}: {post: AppBskyFeedDefs.PostView}) { return ( /* - * The full-width wrapper keeps the social proof on its own line within - * the wrapping stats row, rather than wrapping mid-row and orphaning - * whichever count stat comes last. The link itself hugs its content so - * the empty space to the right of the text is not pressable. + * The full-width wrapper forces the social proof onto its own line below + * the count stats within the wrapping stats row. */ - + {names.length >= 2 ? ( - others > 0 ? ( - - {nameLink(names[0])}, {nameLink(names[1])}, and{' '} - {' '} - like this - - ) : ( - - {nameLink(names[0])} and {nameLink(names[1])} like this - - ) - ) : others > 0 ? ( - - {nameLink(names[0])} and{' '} - {' '} - like this + + Liked by {nameLink(names[0])} and {nameLink(names[1])} ) : ( - - {nameLink(names[0])} likes this + + Liked by {nameLink(names[0])} )} diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index f88ad000c1..7973bc1a8a 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -28,7 +28,7 @@ import {type OnPostSuccessData} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {type PostSource} from '#/state/unstable-post-source' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' -import {LikesStat} from '#/screens/PostThread/components/LikesStat' +import {KnownLikers, LikesStat} from '#/screens/PostThread/components/LikesStat' import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton' import { LINEAR_AVI_WIDTH, @@ -440,7 +440,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ a.py_md, t.atoms.border_contrast_low, ]}> - {post.repostCount != null && post.repostCount !== 0 ? ( ) : null} + {post.bookmarkCount != null && post.bookmarkCount !== 0 ? ( ) : null} + ) : null} Date: Tue, 21 Jul 2026 17:39:51 -0400 Subject: [PATCH 20/32] Hide video pillarboxing on web when the card fits (#11118) --- .../Post/Embed/VideoEmbed/index.web.tsx | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/components/Post/Embed/VideoEmbed/index.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx index 8d5a816021..ee44ba33e8 100644 --- a/src/components/Post/Embed/VideoEmbed/index.web.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx @@ -30,6 +30,13 @@ import * as VideoFallback from './VideoEmbedInner/VideoFallback' const noop = () => {} +/** + * Minimum card width for the overlay controls (play, time, CC, volume, + * fullscreen) to fit without crowding. Narrower cards fall back to the + * full-width pillarbox. + */ +const MIN_CARD_WIDTH = 280 + export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { const t = useTheme() const ref = useRef(null) @@ -91,6 +98,21 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { constrained = Math.max(aspectRatio, ratio) } + const [containerWidth, setContainerWidth] = useState(0) + + /* + * Portrait videos render at their own ratio instead of pillarboxed, but + * only when the resulting card fits the overlay controls. Videos taller + * than 1:2 would still show bars inside a ratio-fit card, and an unknown + * ratio can't be fit, so both keep the full-width pillarbox - a narrow + * card with black slices down the sides looks broken (see #9371). + */ + const cardWidth = containerWidth * Math.min(aspectRatio ?? 1, 1) + const fullBleed = + aspectRatio === undefined || + aspectRatio < 1 / 2 || + (containerWidth > 0 && cardWidth < MIN_CARD_WIDTH) + const contents = (
evt.stopPropagation()}> + {fullBleed && embed.thumbnail && ( + <> + {/* blurred backdrop fills the bars when the video is boxed */} +
+ {/* redraw the sharp thumbnail above the blur */} +
+ + )} + setContainerWidth(e.nativeEvent.layout.width)}> Date: Wed, 22 Jul 2026 03:10:59 +0000 Subject: [PATCH 21/32] Nightly source-language update --- src/locale/locales/en/messages.po | 491 +++++++++++++++--------------- 1 file changed, 244 insertions(+), 247 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index e87e5bf40b..027338657d 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -246,29 +246,6 @@ msgstr "{0} added to chat" msgid "{0} and {1} added to chat" msgstr "{0} and {1} added to chat" -#. Social proof on the likes stat; the bolded names are people the viewer follows who liked the post and are its only likes -#. placeholder {0}: nameLink(names[0]) -#. placeholder {0}: names[0].displayName -#. placeholder {1}: nameLink(names[1]) -#. placeholder {1}: names[1].displayName -#: src/screens/PostThread/components/LikesStat.tsx:135 -#: src/screens/PostThread/components/LikesStat.tsx:191 -msgid "{0} and {1} like this" -msgstr "{0} and {1} like this" - -#. Social proof on the likes stat; the bolded name is a person the viewer follows who liked the post, and the count is the remaining number of likes -#. placeholder {0}: nameLink(names[0]) -#. placeholder {1}: formatPostStatCount(others) -#. placeholder {2}: formatPostStatCount(others) -#: src/screens/PostThread/components/LikesStat.tsx:196 -msgid "{0} and {others, plural, one {{1} other} other {{2} others}} like this" -msgstr "{0} and {others, plural, one {{1} other} other {{2} others}} like this" - -#. placeholder {0}: names[0].displayName -#: src/screens/PostThread/components/LikesStat.tsx:137 -msgid "{0} and {othersLabel} like this" -msgstr "{0} and {othersLabel} like this" - #. placeholder {0}: profile.followsCount || 0 #: src/screens/Profile/Header/Metrics.tsx:49 msgid "{0} following" @@ -319,14 +296,6 @@ msgstr "{0} left" msgid "{0} left the group" msgstr "{0} left the group" -#. Social proof on the likes stat; the bolded name is a person the viewer follows who liked the post and is its only like -#. placeholder {0}: nameLink(names[0]) -#. placeholder {0}: names[0].displayName -#: src/screens/PostThread/components/LikesStat.tsx:138 -#: src/screens/PostThread/components/LikesStat.tsx:206 -msgid "{0} likes this" -msgstr "{0} likes this" - #. placeholder {0}: formatTime(currentTime) #. placeholder {1}: formatTime(duration) #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/Scrubber.tsx:203 @@ -383,21 +352,6 @@ msgstr "{0}, " msgid "{0}, {1} and {memberCount, plural, one {# other} other {# others}} added to chat" msgstr "{0}, {1} and {memberCount, plural, one {# other} other {# others}} added to chat" -#. Social proof on the likes stat; the bolded names are people the viewer follows who liked the post, and the count is the remaining number of likes -#. placeholder {0}: nameLink(names[0]) -#. placeholder {1}: nameLink(names[1]) -#. placeholder {2}: formatPostStatCount(others) -#. placeholder {3}: formatPostStatCount(others) -#: src/screens/PostThread/components/LikesStat.tsx:181 -msgid "{0}, {1}, and {others, plural, one {{2} other} other {{3} others}} like this" -msgstr "{0}, {1}, and {others, plural, one {{2} other} other {{3} others}} like this" - -#. placeholder {0}: names[0].displayName -#. placeholder {1}: names[1].displayName -#: src/screens/PostThread/components/LikesStat.tsx:134 -msgid "{0}, {1}, and {othersLabel} like this" -msgstr "{0}, {1}, and {othersLabel} like this" - #. placeholder {0}: feed.displayName #. placeholder {1}: sanitizeHandle(feed.creatorHandle, '@') #. placeholder {2}: feed.likeCount || 0 @@ -771,12 +725,6 @@ msgstr "" msgid "{numMatches, plural, one {# contact found} other {# contacts found}}" msgstr "" -#. placeholder {0}: formatPostStatCount(others) -#. placeholder {1}: formatPostStatCount(others) -#: src/screens/PostThread/components/LikesStat.tsx:127 -msgid "{others, plural, one {{0} other} other {{1} others}}" -msgstr "{others, plural, one {{0} other} other {{1} others}}" - #: src/components/NewskieDialog.tsx:115 msgid "{profileName} joined Bluesky {timeAgoString} ago" msgstr "" @@ -843,7 +791,7 @@ msgid "+{0}" msgstr "+{0}" #. Indicates the number of additional profiles are in the Starter Pack e.g. +12 -#: src/screens/Search/components/StarterPackCard.tsx:250 +#: src/screens/Search/components/StarterPackCard.tsx:258 msgid "+{computedTotal}" msgstr "" @@ -878,14 +826,14 @@ msgstr "" #. Quote count display, the <0> tags enclose the number of quotes in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.quoteCount) #. placeholder {1}: post.quoteCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:470 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:469 msgid "<0>{0} {1, plural, one {quote} other {quotes}}" msgstr "" #. Repost count display, the <0> tags enclose the number of reposts in bold (will never be 0) #. placeholder {0}: formatPostStatCount(post.repostCount) #. placeholder {1}: post.repostCount -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:449 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:448 msgid "<0>{0} {1, plural, one {repost} other {reposts}}" msgstr "" @@ -898,7 +846,7 @@ msgstr "" #. Like count display, the <0> tags enclose the number of likes in bold (will never be 0) #. placeholder {0}: formatPostStatCount(likeCount) -#: src/screens/PostThread/components/LikesStat.tsx:90 +#: src/screens/PostThread/components/LikesStat.tsx:44 msgid "<0>{0} {likeCount, plural, one {like} other {likes}}" msgstr "<0>{0} {likeCount, plural, one {like} other {likes}}" @@ -926,7 +874,7 @@ msgid "<0>{displayName}<1/><2> added you" msgstr "<0>{displayName}<1/><2> added you" #: src/screens/Hashtag.tsx:230 -#: src/screens/Search/SearchResults.tsx:385 +#: src/screens/Search/SearchResults.tsx:396 msgid "<0>Sign in<1> or <2>create an account<3> <4>to search for news, sports, politics, and everything else happening on Bluesky." msgstr "" @@ -1026,7 +974,7 @@ msgstr "" msgid "A selected recipient is not followed by the sender." msgstr "A selected recipient is not followed by the sender." -#: src/Navigation.tsx:474 +#: src/Navigation.tsx:483 #: src/screens/Settings/AboutSettings.tsx:85 #: src/screens/Settings/Settings.tsx:264 #: src/screens/Settings/Settings.tsx:267 @@ -1075,11 +1023,11 @@ msgstr "Access requested! The group owner will review your request." msgid "Accessibility" msgstr "" -#: src/Navigation.tsx:389 +#: src/Navigation.tsx:390 msgid "Accessibility Settings" msgstr "" -#: src/Navigation.tsx:405 +#: src/Navigation.tsx:406 #: src/screens/Settings/AccountSettings.tsx:54 #: src/screens/Settings/Settings.tsx:174 #: src/screens/Settings/Settings.tsx:177 @@ -1154,11 +1102,15 @@ msgid "Accounts with a scalloped blue check mark <0><1/> can verify others. msgstr "" #: src/lib/hooks/useNotificationHandler.ts:214 -#: src/screens/Settings/NotificationSettings/index.tsx:226 -#: src/screens/Settings/NotificationSettings/index.tsx:365 +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:105 +#: src/screens/Settings/NotificationSettings/index.tsx:225 msgid "Activity from others" msgstr "" +#: src/Navigation.tsx:459 +msgid "Activity notifications" +msgstr "Activity notifications" + #: src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx:191 #: src/components/dialogs/lists/UserAddRemoveListsDialog.tsx:319 #: src/components/dialogs/lists/UserAddRemoveListsDialog.tsx:326 @@ -1635,11 +1587,11 @@ msgstr "" msgid "An error occurred while hiding suggestion. {0}" msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.tsx:188 +#: src/components/Post/Embed/VideoEmbed/index.tsx:197 msgid "An error occurred while loading the video. Please try again later." msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.web.tsx:239 +#: src/components/Post/Embed/VideoEmbed/index.web.tsx:308 msgid "An error occurred while loading the video. Please try again." msgstr "" @@ -1797,7 +1749,7 @@ msgstr "" msgid "Anyone who has it will no longer be able to join or request to join. You can always create a new one." msgstr "Anyone who has it will no longer be able to join or request to join. You can always create a new one." -#: src/Navigation.tsx:482 +#: src/Navigation.tsx:491 #: src/screens/Settings/AppIconSettings/index.tsx:65 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:19 #: src/screens/Settings/AppIconSettings/SettingsListItem.tsx:24 @@ -1835,7 +1787,7 @@ msgstr "" msgid "App passwords" msgstr "" -#: src/Navigation.tsx:357 +#: src/Navigation.tsx:358 #: src/screens/Settings/AppPasswords.tsx:51 msgid "App Passwords" msgstr "" @@ -1886,7 +1838,7 @@ msgstr "" msgid "Appeal this label" msgstr "Appeal this label" -#: src/Navigation.tsx:397 +#: src/Navigation.tsx:398 #: src/screens/Settings/AppearanceSettings.tsx:73 #: src/screens/Settings/Settings.tsx:226 #: src/screens/Settings/Settings.tsx:229 @@ -1904,12 +1856,12 @@ msgid "Apply Pull Request" msgstr "" #. placeholder {0}: niceDate(i18n, createdAt, 'medium') -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:610 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:611 msgid "Archived from {0}" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:581 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:619 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:582 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:620 msgid "Archived post" msgstr "" @@ -2028,7 +1980,7 @@ msgstr "Automatic" msgid "Automation label" msgstr "Automation label" -#: src/Navigation.tsx:421 +#: src/Navigation.tsx:422 #: src/screens/Settings/AutomationLabelSettings.tsx:103 msgid "Automation Label" msgstr "Automation Label" @@ -2155,7 +2107,7 @@ msgstr "" msgid "Beta Feature" msgstr "" -#: src/Navigation.tsx:413 +#: src/Navigation.tsx:414 #: src/screens/Settings/BetaFeaturesSettings.tsx:108 #: src/screens/Settings/Settings.tsx:248 #: src/screens/Settings/Settings.tsx:251 @@ -2264,7 +2216,7 @@ msgstr "" msgid "Blocked accounts" msgstr "" -#: src/Navigation.tsx:193 +#: src/Navigation.tsx:194 #: src/view/screens/ModerationBlockedAccounts.tsx:95 msgid "Blocked Accounts" msgstr "" @@ -2303,7 +2255,7 @@ msgstr "bloomscrolling booksky" msgid "Bluesky" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:635 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:636 msgid "Bluesky cannot confirm the authenticity of the claimed date." msgstr "" @@ -2456,7 +2408,7 @@ msgstr "" #: src/components/LabelingServiceCard/index.tsx:62 #: src/components/moderation/ReportDialog/index.tsx:847 #: src/screens/Messages/JoinRequest.tsx:177 -#: src/screens/Search/components/StarterPackCard.tsx:107 +#: src/screens/Search/components/StarterPackCard.tsx:112 #: src/screens/Search/Explore.tsx:971 msgid "By {0}" msgstr "" @@ -2499,7 +2451,7 @@ msgstr "" msgid "By creating an account you agree to the <0>Terms of Service." msgstr "" -#: src/screens/Search/components/StarterPackCard.tsx:106 +#: src/screens/Search/components/StarterPackCard.tsx:111 msgid "By you" msgstr "" @@ -2693,7 +2645,7 @@ msgid "Changes to the starter pack will not be reflected in the list after creat msgstr "" #: src/lib/hooks/useNotificationHandler.ts:133 -#: src/Navigation.tsx:500 +#: src/Navigation.tsx:509 #: src/view/shell/bottom-bar/BottomBar.tsx:242 #: src/view/shell/desktop/LeftNav.tsx:698 #: src/view/shell/Drawer.tsx:525 @@ -2752,7 +2704,7 @@ msgstr "Chat owners cannot leave a group chat." msgid "Chat recipient is not followed by the sender." msgstr "Chat recipient is not followed by the sender." -#: src/Navigation.tsx:520 +#: src/Navigation.tsx:529 msgid "Chat request inbox" msgstr "" @@ -2762,7 +2714,7 @@ msgid "Chat requests" msgstr "" #: src/components/dms/ConvoMenu.tsx:88 -#: src/Navigation.tsx:515 +#: src/Navigation.tsx:524 #: src/screens/Messages/ChatList.tsx:97 #: src/screens/Messages/ChatList.tsx:101 #: src/screens/Messages/ChatList.tsx:671 @@ -3065,7 +3017,7 @@ msgstr "" #: src/components/ContextMenu/Backdrop.ios.tsx:53 #: src/components/ContextMenu/Backdrop.ios.tsx:79 #: src/components/ContextMenu/Backdrop.tsx:45 -#: src/components/Lightbox/chrome/ImageMenu.tsx:84 +#: src/components/Lightbox/chrome/ImageMenu.tsx:85 msgid "Close menu" msgstr "" @@ -3127,7 +3079,7 @@ msgid "Comics" msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:46 -#: src/Navigation.tsx:347 +#: src/Navigation.tsx:348 #: src/view/screens/CommunityGuidelines.tsx:28 msgid "Community Guidelines" msgstr "" @@ -3248,7 +3200,7 @@ msgstr "" msgid "Content and media" msgstr "" -#: src/Navigation.tsx:458 +#: src/Navigation.tsx:467 msgid "Content and Media" msgstr "" @@ -3485,7 +3437,7 @@ msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:41 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:108 -#: src/Navigation.tsx:352 +#: src/Navigation.tsx:353 #: src/view/screens/CopyrightPolicy.tsx:25 msgid "Copyright Policy" msgstr "" @@ -3621,7 +3573,7 @@ msgstr "" #: src/components/StarterPack/ProfileStarterPacks.tsx:210 #: src/components/StarterPack/ProfileStarterPacks.tsx:319 -#: src/Navigation.tsx:551 +#: src/Navigation.tsx:560 msgid "Create a starter pack" msgstr "" @@ -3652,7 +3604,7 @@ msgstr "" #: src/components/dialogs/Signin.tsx:87 #: src/components/dialogs/Signin.tsx:89 #: src/screens/Hashtag.tsx:236 -#: src/screens/Search/SearchResults.tsx:391 +#: src/screens/Search/SearchResults.tsx:402 msgid "Create an account" msgstr "" @@ -4219,7 +4171,7 @@ msgstr "" msgid "Double tap to close the dialog" msgstr "" -#: src/screens/VideoFeed/index.tsx:1161 +#: src/screens/VideoFeed/index.tsx:1178 msgid "Double tap to like" msgstr "" @@ -4323,6 +4275,7 @@ msgstr "" #: src/screens/Messages/components/EditTextButton.tsx:52 #: src/screens/Settings/AccountSettings.tsx:148 +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:250 #: src/screens/StarterPack/StarterPackScreen.tsx:604 #: src/screens/StarterPack/Wizard/index.tsx:331 #: src/screens/StarterPack/Wizard/index.tsx:336 @@ -4392,7 +4345,7 @@ msgstr "" msgid "Edit moderation list" msgstr "" -#: src/Navigation.tsx:362 +#: src/Navigation.tsx:363 #: src/view/screens/Feeds.tsx:511 msgid "Edit My Feeds" msgstr "" @@ -4401,6 +4354,11 @@ msgstr "" msgid "Edit name" msgstr "Edit name" +#. placeholder {0}: createSanitizedDisplayName( profile, ) +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:242 +msgid "Edit notifications from {0}" +msgstr "Edit notifications from {0}" + #: src/components/StarterPack/Wizard/WizardEditListDialog.tsx:114 msgid "Edit People" msgstr "" @@ -4439,7 +4397,7 @@ msgstr "" msgid "Edit who can reply" msgstr "" -#: src/Navigation.tsx:556 +#: src/Navigation.tsx:565 msgid "Edit your starter pack" msgstr "" @@ -4504,7 +4462,7 @@ msgstr "" msgid "Embed this post in your website. Simply copy the following snippet and paste it into the HTML code of your website." msgstr "" -#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx:64 +#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx:66 msgid "Embedded video player" msgstr "" @@ -4545,12 +4503,13 @@ msgstr "" msgid "Enable media players for" msgstr "" +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:139 #: src/view/screens/Storybook/Admonitions.tsx:76 msgid "Enable notifications for an account by visiting their profile and pressing the <0>bell icon <1/>." msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:136 -#: src/screens/Settings/NotificationSettings/index.tsx:140 +#: src/screens/Settings/NotificationSettings/index.tsx:135 +#: src/screens/Settings/NotificationSettings/index.tsx:139 msgid "Enable push notifications" msgstr "" @@ -4679,7 +4638,7 @@ msgstr "" msgid "Error receiving captcha response." msgstr "" -#: src/screens/Search/SearchResults.tsx:179 +#: src/screens/Search/SearchResults.tsx:190 msgid "Error: {error}" msgstr "" @@ -4706,8 +4665,8 @@ msgctxt "allow messages from" msgid "Everyone" msgstr "Everyone" -#: src/screens/Settings/NotificationSettings/index.tsx:299 -#: src/screens/Settings/NotificationSettings/index.tsx:401 +#: src/screens/Settings/NotificationSettings/index.tsx:298 +#: src/screens/Settings/NotificationSettings/index.tsx:387 msgid "Everything else" msgstr "" @@ -4754,7 +4713,7 @@ msgstr "" msgid "Expand post text" msgstr "" -#: src/screens/VideoFeed/index.tsx:1037 +#: src/screens/VideoFeed/index.tsx:1054 msgid "Expands or collapses post text" msgstr "" @@ -4797,7 +4756,7 @@ msgstr "" msgid "Explicit sexual images." msgstr "" -#: src/Navigation.tsx:760 +#: src/Navigation.tsx:769 #: src/screens/Search/Shell.tsx:541 #: src/view/shell/desktop/LeftNav.tsx:677 #: src/view/shell/Drawer.tsx:473 @@ -4839,7 +4798,7 @@ msgstr "" msgid "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." msgstr "" -#: src/Navigation.tsx:381 +#: src/Navigation.tsx:382 #: src/screens/Settings/ExternalMediaPreferences.tsx:35 msgid "External Media Preferences" msgstr "" @@ -4989,9 +4948,10 @@ msgstr "" #: src/components/dialogs/NotificationSettingsDialog.tsx:85 #: src/screens/Messages/Settings.tsx:369 -#: src/screens/Settings/NotificationSettings/index.tsx:149 -#: src/screens/Settings/NotificationSettings/index.tsx:268 -#: src/screens/Settings/NotificationSettings/index.tsx:285 +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:112 +#: src/screens/Settings/NotificationSettings/index.tsx:148 +#: src/screens/Settings/NotificationSettings/index.tsx:267 +#: src/screens/Settings/NotificationSettings/index.tsx:284 msgid "Failed to load notification settings." msgstr "" @@ -5186,7 +5146,7 @@ msgstr "" msgid "False information about elections" msgstr "" -#: src/Navigation.tsx:292 +#: src/Navigation.tsx:293 msgid "Feed" msgstr "" @@ -5231,10 +5191,10 @@ msgctxt "toast" msgid "Feedback sent to feed operator" msgstr "" -#: src/Navigation.tsx:536 +#: src/Navigation.tsx:545 #: src/screens/SavedFeeds.tsx:112 #: src/screens/SavedFeeds.tsx:303 -#: src/screens/Search/SearchResults.tsx:104 +#: src/screens/Search/SearchResults.tsx:106 #: src/screens/StarterPack/StarterPackScreen.tsx:196 #: src/view/screens/Feeds.tsx:504 #: src/view/screens/Profile.tsx:239 @@ -5347,8 +5307,8 @@ msgstr "" msgid "Find and invite friends" msgstr "Find and invite friends" -#: src/Navigation.tsx:445 -#: src/Navigation.tsx:578 +#: src/Navigation.tsx:446 +#: src/Navigation.tsx:587 msgid "Find Contacts" msgstr "" @@ -5424,7 +5384,7 @@ msgstr "Focus the search field" #: src/screens/Messages/ConversationSettings/Member.tsx:170 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:157 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:409 -#: src/screens/VideoFeed/index.tsx:920 +#: src/screens/VideoFeed/index.tsx:937 #: src/view/com/notifications/NotificationFeedItem.tsx:864 #: src/view/com/notifications/NotificationFeedItem.tsx:871 msgid "Follow" @@ -5440,7 +5400,7 @@ msgstr "" msgid "Follow {displayName}" msgstr "Follow {displayName}" -#: src/screens/VideoFeed/index.tsx:899 +#: src/screens/VideoFeed/index.tsx:916 msgid "Follow {handle}" msgstr "" @@ -5523,7 +5483,7 @@ msgid "Followers can join" msgstr "Followers can join" #. placeholder {0}: route.params.name -#: src/Navigation.tsx:246 +#: src/Navigation.tsx:247 msgid "Followers of @{0} that you know" msgstr "" @@ -5539,7 +5499,7 @@ msgstr "" #: src/components/ProfileHoverCard/index.web.tsx:505 #: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:160 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:405 -#: src/screens/VideoFeed/index.tsx:918 +#: src/screens/VideoFeed/index.tsx:935 #: src/view/com/notifications/NotificationFeedItem.tsx:842 #: src/view/com/notifications/NotificationFeedItem.tsx:859 msgid "Following" @@ -5564,7 +5524,7 @@ msgstr "" msgid "Following {displayName}" msgstr "Following {displayName}" -#: src/screens/VideoFeed/index.tsx:898 +#: src/screens/VideoFeed/index.tsx:915 msgid "Following {handle}" msgstr "" @@ -5573,7 +5533,7 @@ msgstr "" msgid "Following feed preferences" msgstr "" -#: src/Navigation.tsx:368 +#: src/Navigation.tsx:369 #: src/screens/Settings/FollowingFeedPreferences.tsx:57 msgid "Following Feed Preferences" msgstr "" @@ -5713,39 +5673,39 @@ msgstr "Get early access to experimental features we’re testing." msgid "Get help" msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:403 +#: src/screens/Settings/NotificationSettings/index.tsx:389 msgid "Get notifications for starter pack joins, verification, and other activity." msgstr "Get notifications for starter pack joins, verification, and other activity." -#: src/screens/Settings/NotificationSettings/index.tsx:325 +#: src/screens/Settings/NotificationSettings/index.tsx:324 msgid "Get notifications when people follow you." msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:317 +#: src/screens/Settings/NotificationSettings/index.tsx:316 msgid "Get notifications when people like your posts." msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:380 +#: src/screens/Settings/NotificationSettings/index.tsx:366 msgid "Get notifications when people like your reposts." msgstr "Get notifications when people like your reposts." -#: src/screens/Settings/NotificationSettings/index.tsx:341 +#: src/screens/Settings/NotificationSettings/index.tsx:340 msgid "Get notifications when people mention you." msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:349 +#: src/screens/Settings/NotificationSettings/index.tsx:348 msgid "Get notifications when people quote your posts." msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:333 +#: src/screens/Settings/NotificationSettings/index.tsx:332 msgid "Get notifications when people reply to your posts." msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:358 +#: src/screens/Settings/NotificationSettings/index.tsx:357 msgid "Get notifications when people repost your posts." msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:389 +#: src/screens/Settings/NotificationSettings/index.tsx:375 msgid "Get notifications when people repost your reposts." msgstr "Get notifications when people repost your reposts." @@ -5757,15 +5717,15 @@ msgstr "Get notifications when people send you message requests." msgid "Get notifications when people send you messages." msgstr "Get notifications when people send you messages." -#: src/screens/Settings/NotificationSettings/index.tsx:367 -msgid "Get notifications when there's activity on posts you're subscribed to." -msgstr "Get notifications when there's activity on posts you're subscribed to." - #: src/components/activity-notifications/SubscribeProfileButton.tsx:89 #: src/components/activity-notifications/SubscribeProfileButton.tsx:90 msgid "Get notified about new posts" msgstr "" +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:106 +msgid "Get notified about posts and replies from accounts you choose." +msgstr "Get notified about posts and replies from accounts you choose." + #: src/components/activity-notifications/SubscribeProfileDialog.tsx:226 msgid "Get notified of new posts from {name}" msgstr "" @@ -5817,8 +5777,8 @@ msgstr "" #: src/screens/ProfileList/components/ErrorScreen.tsx:35 #: src/screens/ProfileList/components/ErrorScreen.tsx:41 #: src/screens/VideoFeed/components/Header.tsx:163 -#: src/screens/VideoFeed/index.tsx:1222 -#: src/screens/VideoFeed/index.tsx:1226 +#: src/screens/VideoFeed/index.tsx:1239 +#: src/screens/VideoFeed/index.tsx:1243 #: src/view/com/auth/LoggedOut.tsx:127 #: src/view/com/profile/ProfileFollowers.tsx:211 #: src/view/com/profile/ProfileFollowers.tsx:212 @@ -5871,7 +5831,7 @@ msgid "Go live for" msgstr "" #. placeholder {0}: name.displayName -#: src/screens/PostThread/components/LikesStat.tsx:152 +#: src/screens/PostThread/components/LikesStat.tsx:146 msgid "Go to {0}'s profile" msgstr "Go to {0}'s profile" @@ -5975,7 +5935,7 @@ msgctxt "toast" msgid "Group chat name updated" msgstr "Group chat name updated" -#: src/Navigation.tsx:505 +#: src/Navigation.tsx:514 #: src/screens/Messages/ConversationSettings/index.tsx:113 msgid "Group chat settings" msgstr "Group chat settings" @@ -6072,7 +6032,7 @@ msgstr "" msgid "Harming or endangering minors" msgstr "" -#: src/Navigation.tsx:489 +#: src/Navigation.tsx:498 msgid "Hashtag" msgstr "" @@ -6135,7 +6095,7 @@ msgstr "" msgid "Hidden" msgstr "" -#: src/screens/VideoFeed/index.tsx:697 +#: src/screens/VideoFeed/index.tsx:714 msgid "Hidden by your moderation settings." msgstr "" @@ -6288,8 +6248,8 @@ msgstr "" msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!" msgstr "" -#: src/Navigation.tsx:755 -#: src/Navigation.tsx:776 +#: src/Navigation.tsx:764 +#: src/Navigation.tsx:785 #: src/view/shell/bottom-bar/BottomBar.tsx:196 #: src/view/shell/desktop/LeftNav.tsx:667 #: src/view/shell/Drawer.tsx:499 @@ -6404,6 +6364,7 @@ msgstr "" msgid "If you want to change your password, we will send you a code to verify that this is your account." msgstr "" +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:157 #: src/view/screens/Storybook/Admonitions.tsx:90 msgid "If you want to restrict who can receive notifications for your account's activity, you can change this in <0>Settings β†’ Privacy and Security." msgstr "" @@ -6859,7 +6820,7 @@ msgstr "" msgid "Language" msgstr "Language" -#: src/Navigation.tsx:219 +#: src/Navigation.tsx:220 msgid "Language Settings" msgstr "" @@ -6884,7 +6845,7 @@ msgid "Last initiated just now" msgstr "" #: src/screens/Hashtag.tsx:97 -#: src/screens/Search/SearchResults.tsx:86 +#: src/screens/Search/SearchResults.tsx:88 #: src/screens/Topic.tsx:64 msgid "Latest" msgstr "" @@ -6903,7 +6864,7 @@ msgstr "" msgid "Learn More" msgstr "" -#: src/screens/Search/SearchResults.tsx:246 +#: src/screens/Search/SearchResults.tsx:257 msgctxt "english-only-resource" msgid "Learn more about <0>how to use advanced search." msgstr "Learn more about <0>how to use advanced search." @@ -7088,8 +7049,8 @@ msgstr "" msgid "Like this labeler" msgstr "" -#: src/Navigation.tsx:297 -#: src/Navigation.tsx:302 +#: src/Navigation.tsx:298 +#: src/Navigation.tsx:303 msgid "Liked by" msgstr "" @@ -7106,6 +7067,24 @@ msgstr "" msgid "Liked by {0, plural, one {# user} other {# users}}" msgstr "" +#. Social proof below the post stats; the bolded name is a person the viewer follows who liked the post +#. placeholder {0}: nameLink(names[0]) +#. placeholder {0}: names[0].displayName +#: src/screens/PostThread/components/LikesStat.tsx:132 +#: src/screens/PostThread/components/LikesStat.tsx:173 +msgid "Liked by {0}" +msgstr "Liked by {0}" + +#. Social proof below the post stats; the bolded names are people the viewer follows who liked the post +#. placeholder {0}: nameLink(names[0]) +#. placeholder {0}: names[0].displayName +#. placeholder {1}: nameLink(names[1]) +#. placeholder {1}: names[1].displayName +#: src/screens/PostThread/components/LikesStat.tsx:131 +#: src/screens/PostThread/components/LikesStat.tsx:169 +msgid "Liked by {0} and {1}" +msgstr "Liked by {0} and {1}" + #: src/components/LabelingServiceCard/index.tsx:96 #: src/screens/Profile/components/ProfileFeedHeader.tsx:486 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:169 @@ -7114,19 +7093,19 @@ msgid "Liked by {likeCount, plural, one {# user} other {# users}}" msgstr "" #: src/lib/hooks/useNotificationHandler.ts:158 -#: src/screens/Settings/NotificationSettings/index.tsx:160 -#: src/screens/Settings/NotificationSettings/index.tsx:315 +#: src/screens/Settings/NotificationSettings/index.tsx:159 +#: src/screens/Settings/NotificationSettings/index.tsx:314 #: src/view/screens/Profile.tsx:238 msgid "Likes" msgstr "" #: src/lib/hooks/useNotificationHandler.ts:200 -#: src/screens/Settings/NotificationSettings/index.tsx:239 -#: src/screens/Settings/NotificationSettings/index.tsx:378 +#: src/screens/Settings/NotificationSettings/index.tsx:238 +#: src/screens/Settings/NotificationSettings/index.tsx:364 msgid "Likes of your reposts" msgstr "" -#: src/screens/PostThread/components/LikesStat.tsx:85 +#: src/screens/PostThread/components/LikesStat.tsx:39 msgid "Likes on this post" msgstr "" @@ -7139,7 +7118,7 @@ msgstr "" msgid "Link copied to clipboard" msgstr "Link copied to clipboard" -#: src/Navigation.tsx:252 +#: src/Navigation.tsx:253 msgid "List" msgstr "" @@ -7225,7 +7204,7 @@ msgctxt "toast" msgid "List unmuted" msgstr "" -#: src/Navigation.tsx:173 +#: src/Navigation.tsx:174 #: src/view/screens/Lists.tsx:60 #: src/view/screens/Profile.tsx:233 #: src/view/screens/Profile.tsx:241 @@ -7331,7 +7310,7 @@ msgstr "Lock this group chat" msgid "Locked" msgstr "Locked" -#: src/Navigation.tsx:327 +#: src/Navigation.tsx:328 msgid "Log" msgstr "" @@ -7488,8 +7467,8 @@ msgid "mentioned users" msgstr "" #: src/lib/hooks/useNotificationHandler.ts:179 -#: src/screens/Settings/NotificationSettings/index.tsx:193 -#: src/screens/Settings/NotificationSettings/index.tsx:340 +#: src/screens/Settings/NotificationSettings/index.tsx:192 +#: src/screens/Settings/NotificationSettings/index.tsx:339 #: src/view/screens/Notifications.tsx:99 msgid "Mentions" msgstr "" @@ -7558,7 +7537,7 @@ msgstr "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" msgid "Message options" msgstr "" -#: src/Navigation.tsx:770 +#: src/Navigation.tsx:779 msgid "Messages" msgstr "" @@ -7587,7 +7566,7 @@ msgstr "" msgid "Missing media" msgstr "" -#: src/Navigation.tsx:178 +#: src/Navigation.tsx:179 #: src/screens/Moderation/index.tsx:100 msgid "Moderation" msgstr "" @@ -7631,7 +7610,7 @@ msgstr "" msgid "Moderation lists" msgstr "" -#: src/Navigation.tsx:183 +#: src/Navigation.tsx:184 #: src/view/screens/ModerationModlists.tsx:60 msgid "Moderation Lists" msgstr "" @@ -7640,7 +7619,7 @@ msgstr "" msgid "moderation settings" msgstr "" -#: src/Navigation.tsx:312 +#: src/Navigation.tsx:313 msgid "Moderation states" msgstr "" @@ -7781,7 +7760,7 @@ msgstr "Muted" msgid "Muted accounts" msgstr "" -#: src/Navigation.tsx:188 +#: src/Navigation.tsx:189 #: src/view/screens/ModerationMutedAccounts.tsx:107 msgid "Muted Accounts" msgstr "" @@ -7926,8 +7905,8 @@ msgid "New Feature" msgstr "" #: src/lib/hooks/useNotificationHandler.ts:193 -#: src/screens/Settings/NotificationSettings/index.tsx:171 -#: src/screens/Settings/NotificationSettings/index.tsx:324 +#: src/screens/Settings/NotificationSettings/index.tsx:170 +#: src/screens/Settings/NotificationSettings/index.tsx:323 msgid "New followers" msgstr "" @@ -7961,13 +7940,13 @@ msgstr "" #: src/screens/Messages/Settings.tsx:289 #: src/screens/Settings/NotificationSettings/components/ChatNotificationDialogs.tsx:31 -#: src/screens/Settings/NotificationSettings/index.tsx:282 +#: src/screens/Settings/NotificationSettings/index.tsx:281 msgid "New message requests" msgstr "New message requests" #: src/screens/Messages/Settings.tsx:268 #: src/screens/Settings/NotificationSettings/components/ChatNotificationDialogs.tsx:21 -#: src/screens/Settings/NotificationSettings/index.tsx:265 +#: src/screens/Settings/NotificationSettings/index.tsx:264 msgid "New messages" msgstr "New messages" @@ -8231,15 +8210,15 @@ msgstr "" msgid "No results found for \"{query}\"" msgstr "" -#: src/screens/Search/SearchResults.tsx:206 +#: src/screens/Search/SearchResults.tsx:217 msgid "No results found for β€œ<0>{query}” with advanced search filters applied." msgstr "No results found for β€œ<0>{query}” with advanced search filters applied." -#: src/screens/Search/SearchResults.tsx:218 +#: src/screens/Search/SearchResults.tsx:229 msgid "No results found for β€œ<0>{query}”." msgstr "No results found for β€œ<0>{query}”." -#: src/screens/Search/SearchResults.tsx:212 +#: src/screens/Search/SearchResults.tsx:223 msgid "No results found for your query with advanced search filters applied." msgstr "No results found for your query with advanced search filters applied." @@ -8293,6 +8272,10 @@ msgstr "" msgid "Non-sexual Nudity" msgstr "" +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:220 +msgid "None" +msgstr "None" + #: src/screens/Search/components/AdvancedSearchDialog/index.tsx:263 #: src/screens/Search/components/AdvancedSearchDialog/index.tsx:266 msgid "None of these words" @@ -8311,7 +8294,7 @@ msgstr "" msgid "Not followed by anyone you’re following" msgstr "Not followed by anyone you’re following" -#: src/Navigation.tsx:168 +#: src/Navigation.tsx:169 #: src/view/screens/Profile.tsx:132 msgid "Not Found" msgstr "" @@ -8337,8 +8320,8 @@ msgid "Nothing saved yet" msgstr "" #: src/components/dialogs/NotificationSettingsDialog.tsx:72 -#: src/Navigation.tsx:452 -#: src/Navigation.tsx:531 +#: src/Navigation.tsx:453 +#: src/Navigation.tsx:540 #: src/view/screens/Notifications.tsx:134 msgid "Notification settings" msgstr "" @@ -8348,11 +8331,12 @@ msgstr "" msgid "Notification sounds" msgstr "" -#: src/Navigation.tsx:526 -#: src/Navigation.tsx:765 +#: src/Navigation.tsx:535 +#: src/Navigation.tsx:774 #: src/screens/Messages/Settings.tsx:255 #: src/screens/Notifications/ActivityList.tsx:31 -#: src/screens/Settings/NotificationSettings/index.tsx:126 +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:93 +#: src/screens/Settings/NotificationSettings/index.tsx:125 #: src/screens/Settings/Settings.tsx:198 #: src/screens/Settings/Settings.tsx:201 #: src/view/screens/Notifications.tsx:128 @@ -8402,7 +8386,7 @@ msgstr "" #: src/components/dms/InitiateChatFlow.tsx:733 #: src/components/dms/MessageItem.tsx:742 #: src/screens/Login/PasswordUpdatedForm.tsx:35 -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:641 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:642 msgid "Okay" msgstr "" @@ -8622,7 +8606,7 @@ msgstr "" msgid "Open muted words and tags settings" msgstr "" -#: src/screens/Search/components/StarterPackCard.tsx:120 +#: src/screens/Search/components/StarterPackCard.tsx:128 msgid "Open pack" msgstr "" @@ -8925,7 +8909,7 @@ msgid "Pause video" msgstr "" #: src/screens/ProfileList/index.tsx:167 -#: src/screens/Search/SearchResults.tsx:98 +#: src/screens/Search/SearchResults.tsx:100 #: src/screens/StarterPack/StarterPackScreen.tsx:195 msgid "People" msgstr "" @@ -8939,12 +8923,12 @@ msgid "People {ownerName} follows can request to join" msgstr "People {ownerName} follows can request to join" #. placeholder {0}: route.params.name -#: src/Navigation.tsx:239 +#: src/Navigation.tsx:240 msgid "People followed by @{0}" msgstr "" #. placeholder {0}: route.params.name -#: src/Navigation.tsx:232 +#: src/Navigation.tsx:233 msgid "People following @{0}" msgstr "" @@ -9071,7 +9055,7 @@ msgstr "" msgid "Play GIF" msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.tsx:169 +#: src/components/Post/Embed/VideoEmbed/index.tsx:178 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:333 msgid "Play video" msgstr "" @@ -9298,10 +9282,10 @@ msgid "Post blocked" msgstr "" #. placeholder {0}: route.params.name -#: src/Navigation.tsx:265 -#: src/Navigation.tsx:272 -#: src/Navigation.tsx:279 -#: src/Navigation.tsx:286 +#: src/Navigation.tsx:266 +#: src/Navigation.tsx:273 +#: src/Navigation.tsx:280 +#: src/Navigation.tsx:287 msgid "Post by @{0}" msgstr "" @@ -9335,7 +9319,7 @@ msgstr "" msgid "Post interaction settings" msgstr "" -#: src/Navigation.tsx:199 +#: src/Navigation.tsx:200 #: src/screens/ModerationInteractionSettings/index.tsx:35 msgid "Post Interaction Settings" msgstr "" @@ -9372,6 +9356,7 @@ msgstr "" #: src/components/activity-notifications/SubscribeProfileDialog.tsx:257 #: src/components/activity-notifications/SubscribeProfileDialog.tsx:269 #: src/screens/ProfileList/index.tsx:167 +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:216 #: src/screens/StarterPack/StarterPackScreen.tsx:197 #: src/view/screens/Profile.tsx:234 msgid "Posts" @@ -9389,6 +9374,10 @@ msgstr "" msgid "Posts hidden" msgstr "" +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:214 +msgid "Posts, Replies" +msgstr "Posts, Replies" + #: src/components/dialogs/LinkWarning.tsx:89 msgid "Potentially misleading link" msgstr "" @@ -9440,20 +9429,21 @@ msgstr "" msgid "Privacy and security" msgstr "" -#: src/Navigation.tsx:429 -#: src/Navigation.tsx:437 +#: src/Navigation.tsx:430 +#: src/Navigation.tsx:438 #: src/screens/Settings/ActivityPrivacySettings.tsx:41 #: src/screens/Settings/PrivacyAndSecuritySettings.tsx:45 msgid "Privacy and Security" msgstr "" +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:161 #: src/view/screens/Storybook/Admonitions.tsx:94 msgid "Privacy and Security settings" msgstr "" #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:36 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:103 -#: src/Navigation.tsx:337 +#: src/Navigation.tsx:338 #: src/screens/Settings/AboutSettings.tsx:102 #: src/screens/Settings/AboutSettings.tsx:105 #: src/view/screens/PrivacyPolicy.tsx:25 @@ -9590,12 +9580,12 @@ msgstr "" #: src/lib/hooks/useNotificationHandler.ts:186 #: src/screens/Post/PostQuotes.tsx:31 -#: src/screens/Settings/NotificationSettings/index.tsx:204 -#: src/screens/Settings/NotificationSettings/index.tsx:347 +#: src/screens/Settings/NotificationSettings/index.tsx:203 +#: src/screens/Settings/NotificationSettings/index.tsx:346 msgid "Quotes" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:466 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:465 msgid "Quotes of this post" msgstr "" @@ -9638,7 +9628,7 @@ msgstr "" msgid "Read {0, plural, one {# more reply} other {# more replies}}" msgstr "" -#: src/screens/Search/SearchResults.tsx:249 +#: src/screens/Search/SearchResults.tsx:260 msgctxt "english-only-resource" msgid "Read about how to use advanced search filters" msgstr "Read about how to use advanced search filters" @@ -9648,11 +9638,11 @@ msgstr "Read about how to use advanced search filters" msgid "Read blog post" msgstr "" -#: src/screens/VideoFeed/index.tsx:1038 +#: src/screens/VideoFeed/index.tsx:1055 msgid "Read less" msgstr "" -#: src/screens/VideoFeed/index.tsx:1038 +#: src/screens/VideoFeed/index.tsx:1055 msgid "Read more" msgstr "" @@ -10009,8 +9999,9 @@ msgstr "Replied-to message, tap to scroll to it" #: src/components/activity-notifications/SubscribeProfileDialog.tsx:274 #: src/components/activity-notifications/SubscribeProfileDialog.tsx:286 #: src/lib/hooks/useNotificationHandler.ts:172 -#: src/screens/Settings/NotificationSettings/index.tsx:182 -#: src/screens/Settings/NotificationSettings/index.tsx:331 +#: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:218 +#: src/screens/Settings/NotificationSettings/index.tsx:181 +#: src/screens/Settings/NotificationSettings/index.tsx:330 #: src/view/screens/Profile.tsx:235 msgid "Replies" msgstr "" @@ -10198,18 +10189,18 @@ msgid "Reposted by you" msgstr "" #: src/lib/hooks/useNotificationHandler.ts:165 -#: src/screens/Settings/NotificationSettings/index.tsx:215 -#: src/screens/Settings/NotificationSettings/index.tsx:356 +#: src/screens/Settings/NotificationSettings/index.tsx:214 +#: src/screens/Settings/NotificationSettings/index.tsx:355 msgid "Reposts" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:445 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:444 msgid "Reposts of this post" msgstr "" #: src/lib/hooks/useNotificationHandler.ts:207 -#: src/screens/Settings/NotificationSettings/index.tsx:252 -#: src/screens/Settings/NotificationSettings/index.tsx:387 +#: src/screens/Settings/NotificationSettings/index.tsx:251 +#: src/screens/Settings/NotificationSettings/index.tsx:373 msgid "Reposts of your reposts" msgstr "" @@ -10244,7 +10235,7 @@ msgstr "Requested" msgid "Requests" msgstr "Requests" -#: src/Navigation.tsx:510 +#: src/Navigation.tsx:519 #: src/screens/Messages/JoinRequests.tsx:59 #: src/screens/Messages/JoinRequests.tsx:425 msgid "Requests to join" @@ -10386,7 +10377,7 @@ msgstr "" #: src/screens/ProfileList/components/ErrorScreen.tsx:36 #: src/screens/Settings/components/ChangeHandleDialog.tsx:577 -#: src/screens/VideoFeed/index.tsx:1223 +#: src/screens/VideoFeed/index.tsx:1240 #: src/view/screens/NotFound.tsx:54 msgid "Returns to previous page" msgstr "" @@ -10457,7 +10448,7 @@ msgstr "" msgid "Save draft?" msgstr "" -#: src/components/Lightbox/chrome/ImageMenu.tsx:98 +#: src/components/Lightbox/chrome/ImageMenu.tsx:99 #: src/components/MediaPreview.tsx:231 #: src/components/Post/Embed/ImageContextMenu.tsx:70 #: src/components/StarterPack/ShareDialog.tsx:144 @@ -10495,7 +10486,7 @@ msgid "Saved Feeds" msgstr "" #: src/components/dialogs/nuxs/BookmarksAnnouncement.tsx:145 -#: src/Navigation.tsx:570 +#: src/Navigation.tsx:579 #: src/screens/Bookmarks.tsx:59 msgid "Saved Posts" msgstr "" @@ -10526,7 +10517,7 @@ msgstr "Scan" #: src/features/inviteFriends/InviteScannerScreen.tsx:82 #: src/features/inviteFriends/InviteScannerScreen.web.tsx:23 -#: src/Navigation.tsx:317 +#: src/Navigation.tsx:318 msgid "Scan QR code" msgstr "Scan QR code" @@ -10564,7 +10555,7 @@ msgstr "" #. placeholder {0}: profile.handle #. placeholder {0}: route.params.name -#: src/Navigation.tsx:258 +#: src/Navigation.tsx:259 #: src/screens/Profile/ProfileSearch.tsx:37 msgid "Search @{0}'s posts" msgstr "" @@ -10621,7 +10612,7 @@ msgid "Search GIFs" msgstr "" #: src/screens/Hashtag.tsx:228 -#: src/screens/Search/SearchResults.tsx:383 +#: src/screens/Search/SearchResults.tsx:394 msgid "Search is currently unavailable when logged out" msgstr "" @@ -11012,14 +11003,14 @@ msgstr "Set your hosting provider manually" msgid "Sets email for password reset" msgstr "" -#: src/Navigation.tsx:214 +#: src/Navigation.tsx:215 #: src/screens/Settings/Settings.tsx:99 #: src/view/shell/desktop/LeftNav.tsx:755 #: src/view/shell/Drawer.tsx:668 msgid "Settings" msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:221 +#: src/screens/Settings/NotificationSettings/index.tsx:220 msgid "Settings for activity from others" msgstr "" @@ -11027,49 +11018,49 @@ msgstr "" msgid "Settings for allowing others to be notified of your posts" msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:155 +#: src/screens/Settings/NotificationSettings/index.tsx:154 msgid "Settings for like notifications" msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:188 +#: src/screens/Settings/NotificationSettings/index.tsx:187 msgid "Settings for mention notifications" msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:166 +#: src/screens/Settings/NotificationSettings/index.tsx:165 msgid "Settings for new follower notifications" msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:294 +#: src/screens/Settings/NotificationSettings/index.tsx:293 msgid "Settings for notifications for everything else" msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:234 +#: src/screens/Settings/NotificationSettings/index.tsx:233 msgid "Settings for notifications for likes of your reposts" msgstr "" #: src/screens/Messages/Settings.tsx:280 -#: src/screens/Settings/NotificationSettings/index.tsx:277 +#: src/screens/Settings/NotificationSettings/index.tsx:276 msgid "Settings for notifications for new message requests" msgstr "Settings for notifications for new message requests" #: src/screens/Messages/Settings.tsx:259 -#: src/screens/Settings/NotificationSettings/index.tsx:260 +#: src/screens/Settings/NotificationSettings/index.tsx:259 msgid "Settings for notifications for new messages" msgstr "Settings for notifications for new messages" -#: src/screens/Settings/NotificationSettings/index.tsx:247 +#: src/screens/Settings/NotificationSettings/index.tsx:246 msgid "Settings for notifications for reposts of your reposts" msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:199 +#: src/screens/Settings/NotificationSettings/index.tsx:198 msgid "Settings for quote notifications" msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:177 +#: src/screens/Settings/NotificationSettings/index.tsx:176 msgid "Settings for reply notifications" msgstr "" -#: src/screens/Settings/NotificationSettings/index.tsx:210 +#: src/screens/Settings/NotificationSettings/index.tsx:209 msgid "Settings for repost notifications" msgstr "" @@ -11118,7 +11109,7 @@ msgstr "" msgid "Share feedback" msgstr "Share feedback" -#: src/components/Lightbox/chrome/ImageMenu.tsx:93 +#: src/components/Lightbox/chrome/ImageMenu.tsx:94 #: src/components/Lightbox/Lightbox.web.tsx:281 #: src/components/Lightbox/Lightbox.web.tsx:307 msgid "Share image" @@ -11196,7 +11187,7 @@ msgstr "" msgid "Share your thoughts…" msgstr "Share your thoughts…" -#: src/Navigation.tsx:322 +#: src/Navigation.tsx:323 msgid "Shared Preferences Tester" msgstr "" @@ -11225,8 +11216,8 @@ msgstr "" #: src/features/liveNow/components/LiveStatusDialog.tsx:318 #: src/features/liveNow/components/LiveStatusDialog.tsx:322 #: src/screens/List/ListHiddenScreen.tsx:194 -#: src/screens/VideoFeed/index.tsx:700 -#: src/screens/VideoFeed/index.tsx:706 +#: src/screens/VideoFeed/index.tsx:717 +#: src/screens/VideoFeed/index.tsx:723 msgid "Show anyway" msgstr "" @@ -11324,7 +11315,7 @@ msgstr "" msgid "Show when you’re live" msgstr "" -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:582 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:583 msgid "Shows information about when this post was created" msgstr "" @@ -11349,7 +11340,7 @@ msgstr "" #: src/screens/Login/LoginForm.tsx:560 #: src/screens/Messages/JoinRequest.tsx:290 #: src/screens/Messages/JoinRequest.tsx:296 -#: src/screens/Search/SearchResults.tsx:386 +#: src/screens/Search/SearchResults.tsx:397 #: src/view/com/auth/SplashScreen.tsx:116 #: src/view/com/auth/SplashScreen.tsx:123 #: src/view/com/auth/SplashScreen.web.tsx:124 @@ -11682,8 +11673,8 @@ msgstr "Start chat" msgid "Start chat with {displayName}" msgstr "" -#: src/Navigation.tsx:541 -#: src/Navigation.tsx:546 +#: src/Navigation.tsx:550 +#: src/Navigation.tsx:555 #: src/screens/StarterPack/Wizard/index.tsx:197 msgid "Starter Pack" msgstr "" @@ -11706,6 +11697,10 @@ msgstr "" msgid "Starter pack is invalid" msgstr "" +#: src/screens/Search/SearchResults.tsx:112 +msgid "Starter packs" +msgstr "Starter packs" + #: src/screens/Search/Explore.tsx:665 #: src/view/screens/Profile.tsx:240 msgid "Starter Packs" @@ -11745,7 +11740,7 @@ msgstr "" msgid "Stored as part of a secure code for matching with others" msgstr "" -#: src/Navigation.tsx:307 +#: src/Navigation.tsx:308 #: src/screens/Settings/Settings.tsx:465 msgid "Storybook" msgstr "" @@ -11874,7 +11869,7 @@ msgctxt "Name of app icon variant" msgid "Sunset" msgstr "" -#: src/Navigation.tsx:332 +#: src/Navigation.tsx:333 #: src/view/screens/Support.tsx:25 #: src/view/screens/Support.tsx:28 msgid "Support" @@ -12051,7 +12046,7 @@ msgstr "" #: src/components/dialogs/BirthDateSettings.tsx:181 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:31 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:98 -#: src/Navigation.tsx:342 +#: src/Navigation.tsx:343 #: src/screens/Settings/AboutSettings.tsx:94 #: src/screens/Settings/AboutSettings.tsx:97 #: src/view/screens/TermsOfService.tsx:25 @@ -12108,7 +12103,7 @@ msgstr "" msgid "That's all, folks!" msgstr "" -#: src/screens/VideoFeed/index.tsx:1195 +#: src/screens/VideoFeed/index.tsx:1212 msgid "That's everything!" msgstr "" @@ -12632,7 +12627,7 @@ msgstr "This person is blocking you" #. placeholder {0}: niceDate(i18n, createdAt) #. placeholder {1}: niceDate(i18n, indexedAt) -#: src/screens/PostThread/components/ThreadItemAnchor.tsx:622 +#: src/screens/PostThread/components/ThreadItemAnchor.tsx:623 msgid "This post claims to have been created on <0>{0}, but was first seen by Bluesky on <1>{1}." msgstr "" @@ -12728,7 +12723,7 @@ msgstr "" msgid "This user isn't following anyone." msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.web.tsx:236 +#: src/components/Post/Embed/VideoEmbed/index.web.tsx:304 msgid "This video can’t be played on your device. Your browser or system may be missing the required video codecs (H.264/AAC)." msgstr "This video can’t be played on your device. Your browser or system may be missing the required video codecs (H.264/AAC)." @@ -12777,7 +12772,7 @@ msgstr "" msgid "Threaded" msgstr "" -#: src/Navigation.tsx:375 +#: src/Navigation.tsx:376 msgid "Threads Preferences" msgstr "" @@ -12837,7 +12832,7 @@ msgid "Too many contacts - you've exceeded the number of contacts you can import msgstr "" #: src/screens/Hashtag.tsx:86 -#: src/screens/Search/SearchResults.tsx:74 +#: src/screens/Search/SearchResults.tsx:76 #: src/screens/Topic.tsx:58 msgid "Top" msgstr "" @@ -12849,7 +12844,7 @@ msgstr "" msgid "Top replies first" msgstr "" -#: src/Navigation.tsx:494 +#: src/Navigation.tsx:503 msgid "Topic" msgstr "" @@ -12909,11 +12904,11 @@ msgstr "" msgid "Trust emerges from relationships, communities, and shared context, so we’re also enabling <0>trusted verifiers: organizations that can directly issue verification." msgstr "" -#: src/screens/Search/SearchResults.tsx:233 +#: src/screens/Search/SearchResults.tsx:244 msgid "Try a different search term or remove some filters." msgstr "Try a different search term or remove some filters." -#: src/screens/Search/SearchResults.tsx:235 +#: src/screens/Search/SearchResults.tsx:246 msgid "Try a different search term." msgstr "Try a different search term." @@ -13095,7 +13090,7 @@ msgstr "" msgid "Unfollow account" msgstr "" -#: src/screens/VideoFeed/index.tsx:902 +#: src/screens/VideoFeed/index.tsx:919 msgid "Unfollows the user" msgstr "" @@ -13501,7 +13496,7 @@ msgstr "" msgid "Verification settings" msgstr "" -#: src/Navigation.tsx:207 +#: src/Navigation.tsx:208 #: src/screens/Moderation/VerificationSettings.tsx:34 msgid "Verification Settings" msgstr "" @@ -13609,7 +13604,7 @@ msgstr "" msgid "Video failed to process" msgstr "" -#: src/Navigation.tsx:562 +#: src/Navigation.tsx:571 msgid "Video Feed" msgstr "" @@ -13619,7 +13614,7 @@ msgid "Video from {0}: {text}" msgstr "" #. placeholder {0}: sanitizeHandle( post.author.handle, '@', ) -#: src/screens/VideoFeed/index.tsx:1157 +#: src/screens/VideoFeed/index.tsx:1174 msgid "Video from {0}. Tap to play or pause the video" msgstr "" @@ -13628,15 +13623,15 @@ msgstr "" msgid "Video Games" msgstr "" -#: src/screens/VideoFeed/index.tsx:1156 +#: src/screens/VideoFeed/index.tsx:1173 msgid "Video is paused" msgstr "" -#: src/screens/VideoFeed/index.tsx:1156 +#: src/screens/VideoFeed/index.tsx:1173 msgid "Video is playing" msgstr "" -#: src/components/Post/Embed/VideoEmbed/index.web.tsx:232 +#: src/components/Post/Embed/VideoEmbed/index.web.tsx:299 msgid "Video not found." msgstr "" @@ -13684,7 +13679,7 @@ msgstr "" #. placeholder {0}: profile.handle #: src/screens/Profile/components/ProfileFeedHeader.tsx:454 #: src/screens/Search/components/SearchProfileCard.tsx:37 -#: src/screens/VideoFeed/index.tsx:863 +#: src/screens/VideoFeed/index.tsx:880 #: src/view/com/notifications/NotificationFeedItem.tsx:619 msgid "View {0}'s profile" msgstr "" @@ -13715,8 +13710,8 @@ msgstr "" msgid "View debug entry" msgstr "" -#: src/screens/VideoFeed/index.tsx:728 -#: src/screens/VideoFeed/index.tsx:746 +#: src/screens/VideoFeed/index.tsx:745 +#: src/screens/VideoFeed/index.tsx:763 msgid "View details" msgstr "" @@ -14055,13 +14050,15 @@ msgstr "" msgid "We're sorry, but we weren't able to load your muted words at this time. Please try again." msgstr "" -#: src/screens/Search/SearchResults.tsx:412 -#: src/screens/Search/SearchResults.tsx:553 +#: src/screens/Search/SearchResults.tsx:423 +#: src/screens/Search/SearchResults.tsx:564 +#: src/screens/Search/SearchResults.tsx:761 msgid "We’re sorry, but your search could not be completed." msgstr "We’re sorry, but your search could not be completed." -#: src/screens/Search/SearchResults.tsx:411 -#: src/screens/Search/SearchResults.tsx:552 +#: src/screens/Search/SearchResults.tsx:422 +#: src/screens/Search/SearchResults.tsx:563 +#: src/screens/Search/SearchResults.tsx:760 msgid "We’re sorry, but your search could not be completed. Please try again in a few minutes." msgstr "We’re sorry, but your search could not be completed. Please try again in a few minutes." @@ -14806,7 +14803,7 @@ msgstr "" msgid "You've reached your daily limit for video uploads (too many videos)" msgstr "" -#: src/screens/VideoFeed/index.tsx:1204 +#: src/screens/VideoFeed/index.tsx:1221 msgid "You've run out of videos to watch. Maybe it's a good time to take a break?" msgstr "" @@ -14921,7 +14918,7 @@ msgstr "Your hosting provider can’t be detected from an email address, so the msgid "Your hosting provider is detected automatically from the username you enter." msgstr "Your hosting provider is detected automatically from the username you enter." -#: src/Navigation.tsx:466 +#: src/Navigation.tsx:475 #: src/screens/Search/modules/ExploreInterestsCard.tsx:68 #: src/screens/Settings/ContentAndMediaSettings.tsx:94 #: src/screens/Settings/ContentAndMediaSettings.tsx:97 From f62e54ec821dc0184aab215fd1e2c0a9b3d0084c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 22 Jul 2026 17:39:11 +0300 Subject: [PATCH 22/32] Use stable tsgolint (#11229) --- package.json | 2 +- pnpm-lock.yaml | 64 ++++++++++++++++++++++----------------------- pnpm-workspace.yaml | 8 ++++++ 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 51289561e4..244c8f46b4 100644 --- a/package.json +++ b/package.json @@ -293,7 +293,7 @@ "jest-junit": "^16.0.0", "lint-staged": "^17.0.8", "oxlint": "^1.73.0", - "oxlint-tsgolint": "^0.24.0", + "oxlint-tsgolint": "^7.0.2001", "prettier": "^3.8.3", "react-native-dotenv": "^3.4.11", "react-refresh": "^0.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1da8629683..3b4f945d0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -829,10 +829,10 @@ importers: version: runtime:24.18.0 oxlint: specifier: ^1.73.0 - version: 1.73.0(oxlint-tsgolint@0.24.0) + version: 1.73.0(oxlint-tsgolint@7.0.2001) oxlint-tsgolint: - specifier: ^0.24.0 - version: 0.24.0 + specifier: ^7.0.2001 + version: 7.0.2001 prettier: specifier: ^3.8.3 version: 3.8.3 @@ -2245,33 +2245,33 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxlint-tsgolint/darwin-arm64@0.24.0': - resolution: {integrity: sha512-C2uMmwK5Bc4ri4ysZ6sA8Rcu+A5zBQTp6ml2u0CLLbRZp4kMFPV3yWk8B5DK9Aw7y9bbjogIm75tUwGLFzlsYQ==} + '@oxlint-tsgolint/darwin-arm64@7.0.2001': + resolution: {integrity: sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.24.0': - resolution: {integrity: sha512-Wgvt/1lRbDxmoNqWQKKcL+UIiqLmdJ+EWLpQa1qzoNVAfNB0PJpa82/8dH1twT/3rSs4zrP5TXPWl4juB71WuQ==} + '@oxlint-tsgolint/darwin-x64@7.0.2001': + resolution: {integrity: sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.24.0': - resolution: {integrity: sha512-PB1rxII7KV83+ASY4sSkXtqvpij6ME66+QCRL49uksi/ofs2Rf/UVboYr095n0Rkbl2wgvlsHGl6DHC361jQUQ==} + '@oxlint-tsgolint/linux-arm64@7.0.2001': + resolution: {integrity: sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.24.0': - resolution: {integrity: sha512-xcz3CxKmjTQLREtE/UShh+ruWmm9nAb7UM9zKcD65BStiuYgOakAKkPHl4YS5DztpVcDrE0+HqbOolTlRKYWmw==} + '@oxlint-tsgolint/linux-x64@7.0.2001': + resolution: {integrity: sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.24.0': - resolution: {integrity: sha512-A2i6ZGBec3i20S7RaxkgHc6r3HYtD5Mn7j/mb22NkTz14u0JuudvTu6JggAnbGMcv8+dBKQI//EasxSPJLD8pw==} + '@oxlint-tsgolint/win32-arm64@7.0.2001': + resolution: {integrity: sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.24.0': - resolution: {integrity: sha512-0ZbGd9qRB6zs82moekaKdEvncRANq49EAwfNX62JpTS46feXUhKAuoyVDvZMj6Rywejylrmmu79Wo6faYCo4Ew==} + '@oxlint-tsgolint/win32-x64@7.0.2001': + resolution: {integrity: sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==} cpu: [x64] os: [win32] @@ -7140,8 +7140,8 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - oxlint-tsgolint@0.24.0: - resolution: {integrity: sha512-giCk5sEvG02d5tzPmFMX3hem8ndzEEu1xvGYS5OwNfO2WGl6ZVxt5LjE0yiMDoz94INI7XkXwgFAQiydPvVHDw==} + oxlint-tsgolint@7.0.2001: + resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==} hasBin: true oxlint@1.73.0: @@ -11088,22 +11088,22 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxlint-tsgolint/darwin-arm64@0.24.0': + '@oxlint-tsgolint/darwin-arm64@7.0.2001': optional: true - '@oxlint-tsgolint/darwin-x64@0.24.0': + '@oxlint-tsgolint/darwin-x64@7.0.2001': optional: true - '@oxlint-tsgolint/linux-arm64@0.24.0': + '@oxlint-tsgolint/linux-arm64@7.0.2001': optional: true - '@oxlint-tsgolint/linux-x64@0.24.0': + '@oxlint-tsgolint/linux-x64@7.0.2001': optional: true - '@oxlint-tsgolint/win32-arm64@0.24.0': + '@oxlint-tsgolint/win32-arm64@7.0.2001': optional: true - '@oxlint-tsgolint/win32-x64@0.24.0': + '@oxlint-tsgolint/win32-x64@7.0.2001': optional: true '@oxlint/binding-android-arm-eabi@1.73.0': @@ -16458,16 +16458,16 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - oxlint-tsgolint@0.24.0: + oxlint-tsgolint@7.0.2001: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.24.0 - '@oxlint-tsgolint/darwin-x64': 0.24.0 - '@oxlint-tsgolint/linux-arm64': 0.24.0 - '@oxlint-tsgolint/linux-x64': 0.24.0 - '@oxlint-tsgolint/win32-arm64': 0.24.0 - '@oxlint-tsgolint/win32-x64': 0.24.0 + '@oxlint-tsgolint/darwin-arm64': 7.0.2001 + '@oxlint-tsgolint/darwin-x64': 7.0.2001 + '@oxlint-tsgolint/linux-arm64': 7.0.2001 + '@oxlint-tsgolint/linux-x64': 7.0.2001 + '@oxlint-tsgolint/win32-arm64': 7.0.2001 + '@oxlint-tsgolint/win32-x64': 7.0.2001 - oxlint@1.73.0(oxlint-tsgolint@0.24.0): + oxlint@1.73.0(oxlint-tsgolint@7.0.2001): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.73.0 '@oxlint/binding-android-arm64': 1.73.0 @@ -16488,7 +16488,7 @@ snapshots: '@oxlint/binding-win32-arm64-msvc': 1.73.0 '@oxlint/binding-win32-ia32-msvc': 1.73.0 '@oxlint/binding-win32-x64-msvc': 1.73.0 - oxlint-tsgolint: 0.24.0 + oxlint-tsgolint: 7.0.2001 p-limit@2.3.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c917be629a..a90336d4b1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -43,3 +43,11 @@ patchedDependencies: minimumReleaseAgeExclude: - '@atproto/*' - '@bsky.app/*' + # todo: remove when old enough + - '@oxlint-tsgolint/darwin-arm64@7.0.2001' + - '@oxlint-tsgolint/darwin-x64@7.0.2001' + - '@oxlint-tsgolint/linux-arm64@7.0.2001' + - '@oxlint-tsgolint/linux-x64@7.0.2001' + - '@oxlint-tsgolint/win32-arm64@7.0.2001' + - '@oxlint-tsgolint/win32-x64@7.0.2001' + - oxlint-tsgolint@7.0.2001 From 0bd6a9961329e9485ebc90d2002d6d52d2f9c9ee Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:51:49 -0700 Subject: [PATCH 23/32] Pass recId with trending topic click metrics (#11232) --- src/analytics/metrics/types.ts | 14 +- src/components/TrendingTopics.tsx | 133 +----------------- src/components/interstitials/Trending.tsx | 20 ++- .../StepSuggestedAccounts/index.tsx | 2 +- src/screens/Search/Explore.tsx | 8 -- .../Search/modules/ExploreRecommendations.tsx | 120 ---------------- .../Search/modules/ExploreTrendingTopics.tsx | 5 +- .../queries/trending/useGetTrendsQuery.ts | 46 ++++-- .../queries/trending/useTrendingTopics.ts | 74 ---------- .../shell/desktop/SidebarTrendingTopics.tsx | 21 ++- 10 files changed, 83 insertions(+), 360 deletions(-) delete mode 100644 src/screens/Search/modules/ExploreRecommendations.tsx delete mode 100644 src/state/queries/trending/useTrendingTopics.ts diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index aa525e6495..ea68ea01e6 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -510,7 +510,7 @@ export type Events = { | 'ProgressGuide' location: 'Card' | 'Profile' | 'FollowAll' recSource?: 'Search' - recId?: number | string + recId?: string position: number suggestedDid: string category: string | null @@ -523,7 +523,7 @@ export type Events = { | 'ProfileHeader' | 'Onboarding' | 'SeeMoreSuggestedUsers' - recId?: number | string + recId?: string position: number suggestedDid: string category: string | null @@ -538,7 +538,7 @@ export type Events = { | 'SeeMoreSuggestedUsers' | 'ProgressGuide' recSource?: 'Search' - recId?: number | string + recId?: string position: number suggestedDid: string category: string | null @@ -550,11 +550,11 @@ export type Events = { | 'ProfileInterstitial' | 'ProfileHeader' | 'Onboarding' - recId?: number | string + recId?: string } 'suggestedUser:dismiss': { logContext: 'DiscoverInterstitial' | 'ProfileInterstitial' | 'ProfileHeader' - recId?: number | string + recId?: string position: number suggestedDid: string } @@ -743,9 +743,7 @@ export type Events = { } 'trendingTopic:click': { context: 'sidebar' | 'interstitial' | 'explore' - } - 'recommendedTopic:click': { - context: 'explore' + recId?: string } 'trendingVideos:show': { context: 'settings' diff --git a/src/components/TrendingTopics.tsx b/src/components/TrendingTopics.tsx index 8d35e126d4..4b045c6e3e 100644 --- a/src/components/TrendingTopics.tsx +++ b/src/components/TrendingTopics.tsx @@ -1,143 +1,20 @@ import {useMemo} from 'react' -import {View} from 'react-native' -import {type AtUri} from '@atproto/api' +import {type AppBskyUnspeccedDefs, type AtUri} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {PressableScale} from '#/lib/custom-animations/PressableScale' // import {makeProfileLink} from '#/lib/routes/links' // import {feedUriToHref} from '#/lib/strings/url-helpers' -// import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' -// import {CloseQuote_Filled_Stroke2_Corner0_Rounded as Quote} from '#/components/icons/Quote' -// import {UserAvatar} from '#/view/com/util/UserAvatar' -import {type TrendingTopic} from '#/state/queries/trending/useTrendingTopics' -import {atoms as a, native, useTheme, type ViewStyleProp} from '#/alf' -import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack' +import {native} from '#/alf' import {Link as InternalLink, type LinkProps} from '#/components/Link' -import {Text} from '#/components/Typography' - -export function TrendingTopic({ - topic: raw, - size, - style, - hovered, -}: { - topic: TrendingTopic - size?: 'large' | 'small' - hovered?: boolean -} & ViewStyleProp) { - const topic = useTopic(raw) - - const isSmall = size === 'small' - const hasIcon = topic.type === 'starter-pack' && !isSmall - const iconSize = 20 - - return ( - - {hasIcon && topic.type === 'starter-pack' && ( - - )} - - {/* - - {topic.type === 'tag' ? ( - - ) : topic.type === 'topic' ? ( - - ) : topic.type === 'feed' ? ( - - ) : ( - - )} - - */} - - - {topic.displayName} - - - ) -} - -export function TrendingTopicSkeleton({ - size = 'large', - index = 0, -}: { - size?: 'large' | 'small' - index?: number -}) { - const t = useTheme() - const isSmall = size === 'small' - return ( - - ) -} export function TrendingTopicLink({ topic: raw, children, ...rest }: { - topic: TrendingTopic + topic: AppBskyUnspeccedDefs.TrendView } & Omit) { const topic = useTopic(raw) @@ -168,7 +45,9 @@ type ParsedTrendingTopic = uri: AtUri } -export function useTopic(raw: TrendingTopic): ParsedTrendingTopic { +export function useTopic( + raw: AppBskyUnspeccedDefs.TrendView, +): ParsedTrendingTopic { const {_} = useLingui() return useMemo(() => { const {topic: displayName, link} = raw diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx index 98e8f77b2a..94c4b37dd6 100644 --- a/src/components/interstitials/Trending.tsx +++ b/src/components/interstitials/Trending.tsx @@ -7,7 +7,7 @@ import { useTrendingSettings, useTrendingSettingsApi, } from '#/state/preferences/trending' -import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' +import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery' import {useTrendingConfig} from '#/state/service-config' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' @@ -20,6 +20,8 @@ import {TrendingTopicLink} from '#/components/TrendingTopics' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +const TRENDING_LIMIT = 14 + export function TrendingInterstitial() { const {enabled} = useTrendingConfig() const {trendingDisabled} = useTrendingSettings() @@ -33,8 +35,15 @@ export function Inner() { const gutters = useGutters([0, 'base', 0, 'base']) const trendingPrompt = Prompt.usePromptControl() const {setTrendingDisabled} = useTrendingSettingsApi() - const {data: trending, error, isLoading} = useTrendingTopics() - const noTopics = !isLoading && !error && !trending?.topics?.length + const { + data: trending, + error, + isLoading, + } = useGetTrendsQuery({ + limit: TRENDING_LIMIT, + refetchOnWindowFocus: true, + }) + const noTopics = !isLoading && !error && !trending?.trends?.length const onConfirmHide = useCallback(() => { ax.metric('trendingTopics:hide', {context: 'interstitial'}) @@ -88,15 +97,16 @@ export function Inner() { {' '} - ) : !trending?.topics ? null : ( + ) : !trending?.trends ? null : ( <> - {trending.topics.map(topic => ( + {trending.trends.map(topic => ( { ax.metric('trendingTopic:click', { context: 'interstitial', + recId: trending.recId, }) }}> diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index eaa68d9254..514e892895 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -370,7 +370,7 @@ function SuggestedProfileCard({ category: string | null onSeen: (did: string, position: number) => void recSource?: 'Search' - recId?: number | string + recId?: string }) { const t = useTheme() const ax = useAnalytics() diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index 95f58645ad..fc6f99bd55 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -48,7 +48,6 @@ import { StarterPackCardSkeleton, } from '#/screens/Search/components/StarterPackCard' import {ExploreInterestsCard} from '#/screens/Search/modules/ExploreInterestsCard' -import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations' import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics' import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos' import {atoms as a, native, platform, useTheme} from '#/alf' @@ -154,10 +153,6 @@ type ExploreScreenItems = type: 'trendingVideos' key: string } - | { - type: 'recommendations' - key: string - } | { type: 'profile' key: string @@ -807,9 +802,6 @@ export function Explore({ case 'trendingVideos': { return } - case 'recommendations': { - return - } case 'profile': { return ( : null -} - -function Inner() { - const t = useTheme() - const ax = useAnalytics() - const gutters = useGutters([0, 'compact']) - const {data: trending, error, isLoading} = useTrendingTopics() - const noRecs = !isLoading && !error && !trending?.suggested?.length - const allFeeds = trending?.suggested && isAllFeeds(trending.suggested) - - return error || noRecs ? null : ( - <> - - - - - - Recommended - - - {!allFeeds ? ( - - - Content from across the network we think you might like. - - - ) : ( - - Feeds we think you might like. - - )} - - - - - - {isLoading ? ( - Array(RECOMMENDATIONS_COUNT) - .fill(0) - .map((_, i) => ) - ) : !trending?.suggested ? null : ( - <> - {trending.suggested.map(topic => ( - { - ax.metric('recommendedTopic:click', {context: 'explore'}) - }}> - {({hovered}) => ( - - )} - - ))} - - )} - - - - ) -} - -function isAllFeeds(topics: AppBskyUnspeccedDefs.TrendingTopic[]) { - return topics.every(topic => { - const segments = topic.link.split('/').slice(1) - return segments[0] === 'profile' && segments[2] === 'feed' - }) -} diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx index 6ae7132f18..5a3f4ab7f4 100644 --- a/src/screens/Search/modules/ExploreTrendingTopics.tsx +++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx @@ -45,7 +45,10 @@ function Inner() { trend={trend} rank={index + 1} onPress={() => { - ax.metric('trendingTopic:click', {context: 'explore'}) + ax.metric('trendingTopic:click', { + context: 'explore', + recId: trending.recId, + }) }} /> ))} diff --git a/src/state/queries/trending/useGetTrendsQuery.ts b/src/state/queries/trending/useGetTrendsQuery.ts index c670802aa7..21fa738f28 100644 --- a/src/state/queries/trending/useGetTrendsQuery.ts +++ b/src/state/queries/trending/useGetTrendsQuery.ts @@ -6,6 +6,7 @@ import { aggregateUserInterests, createBskyTopicsHeader, } from '#/lib/api/feed/utils' +import {logger} from '#/logger' import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' @@ -13,24 +14,43 @@ import {useAgent} from '#/state/session' export const DEFAULT_LIMIT = 5 -export const createGetTrendsQueryKey = () => ['trends'] +type QueryProps = { + limit?: number + refetchOnWindowFocus?: boolean +} -export function useGetTrendsQuery() { +function dedupe(trends: T[]): T[] { + const seen = new Set() + return trends.filter(trend => { + if (seen.has(trend.link)) return false + seen.add(trend.link) + return true + }) +} + +export const createGetTrendsQueryKey = (props: QueryProps = {}) => [ + 'trends', + props.limit ?? DEFAULT_LIMIT, +] + +export function useGetTrendsQuery(props: QueryProps = {}) { const agent = useAgent() const {data: preferences} = usePreferencesQuery() + const limit = props.limit ?? DEFAULT_LIMIT const mutedWords = useMemo(() => { return preferences?.moderationPrefs?.mutedWords || [] }, [preferences?.moderationPrefs]) return useQuery({ enabled: !!preferences, + refetchOnWindowFocus: props.refetchOnWindowFocus, staleTime: STALE.MINUTES.THREE, - queryKey: createGetTrendsQueryKey(), + queryKey: createGetTrendsQueryKey({limit}), queryFn: async () => { const contentLangs = getContentLanguages().join(',') const {data} = await agent.app.bsky.unspecced.getTrends( { - limit: DEFAULT_LIMIT, + limit, }, { headers: { @@ -39,17 +59,23 @@ export function useGetTrendsQuery() { }, }, ) + if (!data.recIdStr) { + logger.debug('useGetTrendsQuery response missing recIdStr') + } return data }, select: useCallback( (data: AppBskyUnspeccedGetTrends.OutputSchema) => { return { - trends: (data.trends ?? []).filter(t => { - return !hasMutedWord({ - mutedWords, - text: t.topic + ' ' + t.displayName + ' ' + t.category, - }) - }), + recId: data.recIdStr, + trends: dedupe( + (data.trends ?? []).filter(t => { + return !hasMutedWord({ + mutedWords, + text: `${t.topic} ${t.displayName} ${t.category}`, + }) + }), + ), } }, [mutedWords], diff --git a/src/state/queries/trending/useTrendingTopics.ts b/src/state/queries/trending/useTrendingTopics.ts deleted file mode 100644 index 81b25e5f94..0000000000 --- a/src/state/queries/trending/useTrendingTopics.ts +++ /dev/null @@ -1,74 +0,0 @@ -import {useCallback, useMemo} from 'react' -import {type AppBskyUnspeccedDefs, hasMutedWord} from '@atproto/api' -import {useQuery} from '@tanstack/react-query' - -import {STALE} from '#/state/queries' -import {usePreferencesQuery} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' - -export type TrendingTopic = AppBskyUnspeccedDefs.TrendingTopic - -type Response = { - topics: TrendingTopic[] - suggested: TrendingTopic[] -} - -export const DEFAULT_LIMIT = 14 - -function dedup(topics: TrendingTopic[]): TrendingTopic[] { - const seen = new Set() - return topics.filter(t => { - if (seen.has(t.link)) return false - seen.add(t.link) - return true - }) -} - -export const trendingTopicsQueryKey = ['trending-topics'] - -export function useTrendingTopics() { - const agent = useAgent() - const {data: preferences} = usePreferencesQuery() - const mutedWords = useMemo( - () => preferences?.moderationPrefs?.mutedWords ?? [], - [preferences?.moderationPrefs?.mutedWords], - ) - - return useQuery({ - refetchOnWindowFocus: true, - staleTime: STALE.MINUTES.THREE, - queryKey: trendingTopicsQueryKey, - async queryFn() { - const {data} = await agent.app.bsky.unspecced.getTrendingTopics({ - limit: DEFAULT_LIMIT, - }) - return { - topics: data.topics ?? [], - suggested: data.suggested ?? [], - } - }, - select: useCallback( - (data: Response) => { - return { - topics: dedup( - data.topics.filter(t => { - return !hasMutedWord({ - mutedWords, - text: `${t.topic} ${t.displayName ?? ''} ${t.description ?? ''}`, - }) - }), - ), - suggested: dedup( - data.suggested.filter(t => { - return !hasMutedWord({ - mutedWords, - text: `${t.topic} ${t.displayName ?? ''} ${t.description ?? ''}`, - }) - }), - ), - } - }, - [mutedWords], - ), - }) -} diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx index f337edb86a..aa7bf4026c 100644 --- a/src/view/shell/desktop/SidebarTrendingTopics.tsx +++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx @@ -5,7 +5,7 @@ import { useTrendingSettings, useTrendingSettingsApi, } from '#/state/preferences/trending' -import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' +import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery' import {useTrendingConfig} from '#/state/service-config' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' @@ -30,8 +30,14 @@ function Inner() { const ax = useAnalytics() const trendingPrompt = Prompt.usePromptControl() const {setTrendingDisabled} = useTrendingSettingsApi() - const {data: trending, error, isLoading} = useTrendingTopics() - const noTopics = !isLoading && !error && !trending?.topics?.length + const { + data: trending, + error, + isLoading, + } = useGetTrendsQuery({ + refetchOnWindowFocus: true, + }) + const noTopics = !isLoading && !error && !trending?.trends?.length const onConfirmHide = () => { ax.metric('trendingTopics:hide', {context: 'sidebar'}) @@ -82,14 +88,17 @@ function Inner() { /> )) - ) : !trending?.topics ? null : ( + ) : !trending?.trends ? null : ( <> - {trending.topics.slice(0, TRENDING_LIMIT).map((topic, i) => ( + {trending.trends.slice(0, TRENDING_LIMIT).map((topic, i) => ( { - ax.metric('trendingTopic:click', {context: 'sidebar'}) + ax.metric('trendingTopic:click', { + context: 'sidebar', + recId: trending.recId, + }) }}> {({hovered}) => ( From 933ba1c37315c14db0c2b5356a08f790034c6ffa Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:22:56 -0700 Subject: [PATCH 24/32] Add feature gate for starter pack search (#11234) --- src/analytics/features/types.ts | 1 + src/screens/Search/SearchResults.tsx | 34 ++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index a781137747..c48cdb90df 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -19,6 +19,7 @@ export enum Features { PostThreadKnownLikersEnable = 'post_thread:known_likers:enable', PostThreadKnownLikersFetchEnable = 'post_thread:known_likers:fetch:enable', CustomLogoJapanEnable = 'custom_logo:japan:enable', + SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable', AATest = 'aa-test', } diff --git a/src/screens/Search/SearchResults.tsx b/src/screens/Search/SearchResults.tsx index ed405d3265..2d6079e2fd 100644 --- a/src/screens/Search/SearchResults.tsx +++ b/src/screens/Search/SearchResults.tsx @@ -54,6 +54,7 @@ let SearchResults = ({ onPageSelected: (page: number) => void headerHeight: number }): React.ReactNode => { + const ax = useAnalytics() const {t: l} = useLingui() /* * People/Feeds visibility keys off post-only filters: a `lang` filter applies @@ -64,6 +65,10 @@ let SearchResults = ({ const activePage = hasPostFilters && activeTab > 1 ? 0 : activeTab const tabShape = hasPostFilters ? 'filtered' : 'plain' + const isStarterPacksEnabled = ax.features.enabled( + ax.features.SearchStarterPacksV2Enable, + ) + const sections = useMemo(() => { if (!query && !hasFilters) return [] /* @@ -108,20 +113,29 @@ let SearchResults = ({ ), }, - noFilters && { - title: l`Starter packs`, - component: ( - - ), - }, + noFilters && + isStarterPacksEnabled && { + title: l`Starter packs`, + component: ( + + ), + }, ].filter(Boolean) as { title: string component: React.ReactNode }[] - }, [l, query, filters, hasFilters, hasPostFilters, activePage]) + }, [ + l, + query, + filters, + hasFilters, + hasPostFilters, + activePage, + isStarterPacksEnabled, + ]) // There may be fewer tabs after changing the search options. const selectedPage = activePage > sections.length - 1 ? 0 : activePage From b2de086c3b814176d4faeca549e4cbf1a129c977 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:58:26 -0700 Subject: [PATCH 25/32] Update presentation of trending topics in Explore (#11227) --- src/screens/Search/Explore.tsx | 73 ++-- .../Search/components/ModuleHeader.tsx | 8 +- .../Search/modules/ExploreInterestsCard.tsx | 32 +- .../Search/modules/ExploreTrendingTopics.tsx | 328 +++++++++--------- 4 files changed, 207 insertions(+), 234 deletions(-) diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index fc6f99bd55..364b7cbca2 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -5,9 +5,7 @@ import { type AppBskyFeedDefs, type AppBskyGraphDefs, } from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import * as bcp47Match from 'bcp-47-match' @@ -78,7 +76,7 @@ import { function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) { const t = useTheme() - const {_} = useLingui() + const {t: l} = useLingui() const handleOnPress = () => { void item.onLoadMore() @@ -86,7 +84,7 @@ function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) { return ( + + ) : ( + + + + + )} + + )} - {hasSession && ( + {!isTrending && hasSession ? ( {isPinned ? ( @@ -300,7 +345,10 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { variant="ghost" shape="square" color="secondary"> - + ) }} @@ -310,23 +358,23 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { + onPress={() => void onTogglePinned()}> {l`Unpin from home`} - + + onPress={() => void onToggleSaved()}> {isSaved ? l`Remove from my feeds` : l`Save to my feeds`} @@ -339,12 +387,12 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { variant="ghost" shape="square" color="secondary" - onPress={onTogglePinned}> - + onPress={() => void onTogglePinned()}> + )} - )} + ) : null} @@ -358,7 +406,8 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { setLikeUri={setLikeUri} likeCount={likeCount} isPinned={isPinned} - onTogglePinned={onTogglePinned} + isTrending={isTrending} + onTogglePinned={() => void onTogglePinned()} isFeedStateChangePending={isFeedStateChangePending} /> @@ -373,6 +422,7 @@ function DialogInner({ setLikeUri, likeCount, isPinned, + isTrending, onTogglePinned, isFeedStateChangePending, }: { @@ -381,6 +431,7 @@ function DialogInner({ setLikeUri: (uri: string) => void likeCount: number isPinned: boolean + isTrending: boolean onTogglePinned: () => void isFeedStateChangePending: boolean }) { @@ -459,7 +510,9 @@ function DialogInner({ style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]} numberOfLines={1} onPress={() => control.close()}> - {sanitizeHandle(info.creatorHandle, '@')} + {info.creatorHandle === TRENDING_HANDLE + ? l`Bluesky` + : sanitizeHandle(info.creatorHandle, '@')} @@ -472,12 +525,13 @@ function DialogInner({ color="secondary" shape="round" onPress={onPressShare}> - + - - {typeof likeCount === 'number' && ( + + {typeof likeCount === 'number' && likeCount > 0 ? ( + - )} - - {hasSession && ( + + ) : null} + {hasSession ? ( <> - - - - + + {isLiked ? Unlike : Like} + + + + + ) : null} @@ -541,7 +601,7 @@ function DialogInner({ Report feed - + @@ -556,7 +616,7 @@ function DialogInner({ )} - )} + ) : null} ) } diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index c0eb25f54a..c3ac1acd53 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -327,7 +327,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { count += page.feeds.length } if (count < limit && (data?.pages.length || 0) < 6) { - query.fetchNextPage() + void query.fetchNextPage() lastPageCountRef.current = data?.pages?.length || 0 } }, [query, limit]) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 2c947ff3ec..bc40880bf4 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -17,6 +17,7 @@ import { AppBskyEmbedImages, AppBskyEmbedVideo, type AppBskyFeedDefs, + type RichText as RichTextType, } from '@atproto/api' import {useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' @@ -50,7 +51,7 @@ import {List, type ListRef} from '#/view/com/util/List' import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types' -import {useBreakpoints, useLayoutBreakpoints} from '#/alf' +import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf' import { AgeAssuranceDismissibleFeedBanner, useInternalState as useAgeAssuranceBannerState, @@ -64,6 +65,7 @@ import {FeedTrendingTopicsInterstitial} from '#/components/interstitials/FeedTre import {TrendingInterstitial} from '#/components/interstitials/Trending' import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils' +import {RichText} from '#/components/RichText' import {useAnalytics} from '#/analytics' import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env' import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner' @@ -105,6 +107,11 @@ type FeedRow = type: 'fallbackMarker' key: string } + | { + type: 'description' + key: string + value: RichTextType + } | { type: 'sliceItem' key: string @@ -194,6 +201,7 @@ const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY let PostFeed = ({ feed, + description, feedParams, ignoreFilterFor, style, @@ -216,6 +224,7 @@ let PostFeed = ({ isVideoFeed = false, }: { feed: FeedDescriptor + description?: RichTextType feedParams?: FeedParams ignoreFilterFor?: string style?: StyleProp @@ -232,13 +241,14 @@ let PostFeed = ({ progressViewOffset?: number desktopFixedHeightOffset?: number ListHeaderComponent?: () => React.ReactElement - extraData?: any + extraData?: Record savedFeedConfig?: AppBskyActorDefs.SavedFeed initialNumToRender?: number isVideoFeed?: boolean lastFetchDate?: () => number }): React.ReactNode => { const ax = useAnalytics() + const t = useTheme() const {t: l} = useLingui() const queryClient = useQueryClient() const {currentAccount, hasSession} = useSession() @@ -389,6 +399,7 @@ let PostFeed = ({ * Cached value of whether the current feed was selected at startup. We don't * want this to update when user swipes. */ + // oxlint-disable-next-line react/hook-use-state const [isCurrentFeedAtStartupSelected] = useState(selectedFeed === feed) const blockedOrMutedAuthors = usePostAuthorShadowFilter( @@ -680,8 +691,17 @@ let PostFeed = ({ } } + if (description?.text) { + arr.unshift({ + key: 'description', + type: 'description', + value: description, + }) + } + return arr }, [ + description, isFetched, isError, isEmpty, @@ -793,6 +813,18 @@ let PostFeed = ({ return } else if (row.type === 'feedShutdownMsg') { return + } else if (row.type === 'description') { + return ( + + ) } else if (row.type === 'interstitialFollows') { return } else if (row.type === 'interstitialProgressGuide') { @@ -893,6 +925,7 @@ let PostFeed = ({ feedTab, feedCacheKey, onPressShowLess, + t, ], ) From 850765bc8d3a5ffc2eafae18f4e448b556553a11 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:56:25 -0700 Subject: [PATCH 28/32] Tweak loading state for block dialog (#11236) --- src/components/moderation/BlockDialog.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/moderation/BlockDialog.tsx b/src/components/moderation/BlockDialog.tsx index dcca625163..a575cd7433 100644 --- a/src/components/moderation/BlockDialog.tsx +++ b/src/components/moderation/BlockDialog.tsx @@ -181,6 +181,7 @@ function BlockDialogInner({ const footer = ( - - Date: Wed, 22 Jul 2026 15:54:46 -0700 Subject: [PATCH 31/32] Enable sharing a post to a new group chat (#11191) --- oxlint-suppressions.json | 13 - src/analytics/metrics/types.ts | 2 +- .../PostControls/ShareMenu/ShareMenuItems.tsx | 29 +-- .../ShareMenu/ShareMenuItems.web.tsx | 28 +-- src/components/dms/InitiateChatFlow.tsx | 237 +++++++++++++++++- src/components/dms/dialogs/NewChatDialog.tsx | 3 +- .../dms/dialogs/ShareViaChatDialog.tsx | 124 ++++++++- 7 files changed, 367 insertions(+), 69 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 4018638cdc..ddcd835fb7 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -282,19 +282,6 @@ "count": 1 } }, - "src/components/PostControls/ShareMenu/ShareMenuItems.tsx": { - "typescript/no-floating-promises": { - "count": 3 - }, - "typescript/no-misused-promises": { - "count": 1 - } - }, - "src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx": { - "typescript/no-floating-promises": { - "count": 3 - } - }, "src/components/PostControls/ShareMenu/index.tsx": { "typescript/no-floating-promises": { "count": 1 diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index ea68ea01e6..b0445db587 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -605,7 +605,7 @@ export type Events = { // Group chat adoption 'groupchat:create': { - logContext: 'NewChatDialog' + logContext: 'NewChatDialog' | 'SendViaChatDialog' } 'groupchat:landingPage:view': { hasSession: boolean diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.tsx b/src/components/PostControls/ShareMenu/ShareMenuItems.tsx index 627034f5d6..dc66340fd3 100644 --- a/src/components/PostControls/ShareMenu/ShareMenuItems.tsx +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.tsx @@ -1,9 +1,7 @@ import {memo, useMemo} from 'react' import * as ExpoClipboard from 'expo-clipboard' import {AtUri} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' @@ -37,7 +35,7 @@ let ShareMenuItems = ({ }: ShareMenuItemsProps): React.ReactNode => { const ax = useAnalytics() const {hasSession} = useSession() - const {_} = useLingui() + const {t: l} = useLingui() const navigation = useNavigation() const sendViaChatControl = useDialogControl() const [devModeEnabled] = useDevMode() @@ -61,7 +59,7 @@ let ShareMenuItems = ({ const onSharePost = () => { ax.metric('share:press:nativeShare', {}) const url = toShareUrl(href) - shareUrl(url) + void shareUrl(url) onShareProp() } @@ -74,7 +72,7 @@ let ShareMenuItems = ({ } else { await ExpoClipboard.setStringAsync(url) } - Toast.show(_(msg`Copied to clipboard`), { + Toast.show(l`Copied to clipboard`, { type: 'success', }) onShareProp() @@ -93,11 +91,11 @@ let ShareMenuItems = ({ } const onShareATURI = () => { - shareText(postUri) + void shareText(postUri) } const onShareAuthorDID = () => { - shareText(postAuthor.did) + void shareText(postAuthor.did) } return ( @@ -113,13 +111,13 @@ let ShareMenuItems = ({ { ax.metric('share:press:openDmSearch', {}) sendViaChatControl.open() }}> - Send via direct message + Send via chat @@ -129,7 +127,7 @@ let ShareMenuItems = ({ Share via... @@ -139,8 +137,8 @@ let ShareMenuItems = ({ + label={l`Copy link to post`} + onPress={() => void onCopyLink()}> Copy link to post @@ -164,7 +162,7 @@ let ShareMenuItems = ({ Share post at:// URI @@ -173,7 +171,7 @@ let ShareMenuItems = ({ Share author DID @@ -183,7 +181,6 @@ let ShareMenuItems = ({ )} - () const embedPostControl = useDialogControl() const sendViaChatControl = useDialogControl() @@ -60,7 +58,7 @@ let ShareMenuItems = ({ const onCopyLink = () => { ax.metric('share:press:copyLink', {}) const url = toShareUrl(href) - shareUrl(url) + void shareUrl(url) onShareProp() } @@ -75,17 +73,17 @@ let ShareMenuItems = ({ const canEmbed = IS_WEB && gtMobile && !hideInPWI const onShareATURI = () => { - shareText(postUri) + void shareText(postUri) } const onShareAuthorDID = () => { - shareText(postAuthor.did) + void shareText(postAuthor.did) } const copyLinkItem = ( Copy link to post @@ -102,13 +100,13 @@ let ShareMenuItems = ({ {hasSession && aa.state.access === aa.Access.Full && ( { ax.metric('share:press:openDmSearch', {}) sendViaChatControl.open() }}> - Send via direct message + Send via chat @@ -117,12 +115,12 @@ let ShareMenuItems = ({ {canEmbed && ( { ax.metric('share:press:embed', {}) embedPostControl.open() }}> - {_(msg`Embed post`)} + {l`Embed post`} )} @@ -142,7 +140,7 @@ let ShareMenuItems = ({ Copy post at:// URI @@ -151,7 +149,7 @@ let ShareMenuItems = ({ Copy author DID @@ -161,7 +159,6 @@ let ShareMenuItems = ({ )} - {canEmbed && ( )} - void onSelectGroupChat: (dids: string[], groupName: string) => void startInGroupChat?: boolean + showRecentConvos?: boolean + onSelectExistingChat?: (convoId: string) => void + sortByMessageDeclaration?: boolean }) { const t = useTheme() const {t: l} = useLingui() @@ -230,6 +252,12 @@ export function InitiateChatFlow({ const inputRef = useRef(null) const accountTooNewPromptControl = Dialog.useDialogControl() + const {data: convos} = useListConvosQuery({ + enabled: showRecentConvos, + status: 'accepted', + lockStatus: 'unlocked', + }) + const {data: chatStatus} = useChatActorStatusQuery() const canCreateGroups = chatStatus?.canCreateGroups ?? true const groupMemberLimit = chatStatus?.groupMemberLimit @@ -281,6 +309,10 @@ export function InitiateChatFlow({ let _items: Item[] = [] const checker = chatState === ChatState.NEW_GROUP_CHAT ? canBeAddedToGroup : canBeMessaged + const messageDeclarationRank = (item: Item) => + item.type === 'profile' && checker(item.profile) ? 0 : 1 + const compareByMessageDeclaration = (a: Item, b: Item) => + messageDeclarationRank(a) - messageDeclarationRank(b) if (isError) { _items.push({ @@ -310,9 +342,9 @@ export function InitiateChatFlow({ }) } - _items = _items.sort(item => { - return item.type === 'profile' && checker(item.profile) ? -1 : 1 - }) + if (sortByMessageDeclaration) { + _items = _items.sort(compareByMessageDeclaration) + } } } else { const placeholders: Item[] = Array(10) @@ -322,7 +354,57 @@ export function InitiateChatFlow({ key: i + '', })) - if (follows) { + if ( + chatState === ChatState.NEW_CHAT && + showRecentConvos && + convos && + follows + ) { + const usedDids = new Set() + + for (const page of convos.pages) { + for (const convoView of page.convos) { + const convo = parseConvoView(convoView, currentAccount?.did) + + if (!convo) continue + + if (convo.kind === 'group') { + _items.push({ + type: 'existingChat', + key: convo.view.id, + convo, + }) + } else { + if (convo.primaryMember.handle === 'missing.invalid') continue + if (usedDids.has(convo.primaryMember.did)) continue + + usedDids.add(convo.primaryMember.did) + + _items.push({ + type: 'existingChat', + key: convo.view.id, + convo, + }) + } + } + } + + let followsItems: ProfileItem[] = [] + + for (const page of follows.pages) { + for (const profile of page.follows) { + if (usedDids.has(profile.did)) continue + if (!checker(profile)) continue + followsItems.push({ + type: 'profile', + key: profile.did, + profile, + }) + } + } + + _items.push(...followsItems) + } else if (follows) { for (const page of follows.pages) { for (const profile of page.follows) { if (!checker(profile)) continue @@ -359,10 +441,19 @@ export function InitiateChatFlow({ _items.unshift({type: 'newGroupChat', key: 'newGroupChat'}) } - return _items + const profileDids = new Set() + + return _items.filter(item => { + if (item.type !== 'profile') return true + if (profileDids.has(item.profile.did)) return false + + profileDids.add(item.profile.did) + return true + }) }, [ isError, chatState, + convos, searchText, l, groupChatProfiles, @@ -370,6 +461,8 @@ export function InitiateChatFlow({ currentAccount?.did, follows, aa.flags.groupChatDisabled, + showRecentConvos, + sortByMessageDeclaration, ]) if (searchText && !isFetching && !items.length && !isError) { @@ -429,6 +522,16 @@ export function InitiateChatFlow({ case 'label': { return } + case 'existingChat': { + return showRecentConvos && onSelectExistingChat ? ( + + ) : null + } case 'profile': { switch (chatState) { case ChatState.NEW_CHAT: @@ -474,6 +577,8 @@ export function InitiateChatFlow({ handlePressNewGroupChat, moderationOpts, onSelectChat, + onSelectExistingChat, + showRecentConvos, ], ) @@ -845,6 +950,114 @@ function NewGroupChatButton({ ) } +function ExistingChatCard({ + convo, + moderationOpts, + onPress, +}: { + convo: ConvoWithDetails + moderationOpts: ModerationOpts + onPress: (convoId: string) => void +}) { + const t = useTheme() + const {t: l} = useLingui() + const enabled = + convo.kind === 'group' ? convo.details.lockStatus === 'unlocked' : true + const name = + convo.kind === 'group' + ? convo.details.name + : createSanitizedDisplayName( + convo.primaryMember, + true, + moderateProfile(convo.primaryMember, moderationOpts).ui( + 'displayName', + ), + ) + + const handleOnPress = useCallback(() => { + onPress(convo.view.id) + }, [onPress, convo.view.id]) + + return ( + + ) +} + function DefaultProfileCard({ profile, moderationOpts, diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx index 74ab6b4c0d..fd9eaa8880 100644 --- a/src/components/dms/dialogs/NewChatDialog.tsx +++ b/src/components/dms/dialogs/NewChatDialog.tsx @@ -89,7 +89,7 @@ export function NewChat({ }, onError: error => { logger.error('Failed to create groupchat', {safeMessage: error}) - let errorMessage = l`An issue occurred creating the group chat, please try again.` + let errorMessage = l`An issue occurred starting the group chat, please try again.` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` } else if ( @@ -184,6 +184,7 @@ export function NewChat({ title={l`New chat`} onSelectChat={onCreateChat} onSelectGroupChat={onCreateGroupChat} + sortByMessageDeclaration startInGroupChat={startInGroupChat} /> ) : ( diff --git a/src/components/dms/dialogs/ShareViaChatDialog.tsx b/src/components/dms/dialogs/ShareViaChatDialog.tsx index 30cd80862d..0463cc694d 100644 --- a/src/components/dms/dialogs/ShareViaChatDialog.tsx +++ b/src/components/dms/dialogs/ShareViaChatDialog.tsx @@ -1,11 +1,17 @@ -import {useCallback} from 'react' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' +import {useCallback, useState} from 'react' +import { + ChatBskyConvoGetConvoForMembers, + ChatBskyGroupCreateGroup, +} from '@atproto/api' +import {useLingui} from '@lingui/react/macro' +import {isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' +import {useCreateGroupChat} from '#/state/queries/messages/create-group-chat' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import * as Dialog from '#/components/Dialog' import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList' +import {InitiateChatFlow} from '#/components/dms/InitiateChatFlow' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' @@ -16,26 +22,39 @@ export function SendViaChatDialog({ control: Dialog.DialogControlProps onSelectChat: (chatId: string) => void }) { + const [flowKey, setFlowKey] = useState(0) + const onClose = useCallback(() => setFlowKey(key => key + 1), []) + return ( + nativeOptions={{fullHeight: true}} + onClose={onClose}> - + ) } function SendViaChatDialogInner({ control, + flowKey, onSelectChat, }: { control: Dialog.DialogControlProps + flowKey: number onSelectChat: (chatId: string) => void }) { - const {_} = useLingui() + const {t: l} = useLingui() const ax = useAnalytics() + + const isGroupChatEnabled = !ax.features.enabled(ax.features.GroupChatsDisable) + const {mutate: createChat} = useGetConvoForMembers({ onSuccess: data => { onSelectChat(data.convo.id) @@ -46,8 +65,74 @@ function SendViaChatDialogInner({ ax.metric('chat:open', {logContext: 'SendViaChatDialog'}) }, onError: error => { - logger.error('Failed to share post to chat', {message: error}) - Toast.show(_(msg`An issue occurred while trying to open the chat`), { + logger.error('Failed to share post to chat', {safeMessage: error}) + let errorMessage = l`An issue occurred starting the chat, please try again.` + if (isNetworkError(error)) { + errorMessage = l`A network error occurred. Please check your internet connection.` + } else if ( + error instanceof ChatBskyConvoGetConvoForMembers.AccountSuspendedError + ) { + errorMessage = l`Suspended accounts cannot participate in chat.` + } else if ( + error instanceof ChatBskyConvoGetConvoForMembers.BlockedActorError + ) { + errorMessage = l`This user has blocked you and cannot be messaged.` + } else if ( + error instanceof ChatBskyConvoGetConvoForMembers.MessagesDisabledError + ) { + errorMessage = l`This user has disabled chat and cannot be messaged.` + } else if ( + error instanceof + ChatBskyConvoGetConvoForMembers.NotFollowedBySenderError + ) { + errorMessage = l`Chat recipient is not followed by the sender.` + } else if ( + error instanceof ChatBskyConvoGetConvoForMembers.RecipientNotFoundError + ) { + errorMessage = l`Unable to find the selected recipient.` + } + Toast.show(errorMessage, { + type: 'error', + }) + }, + }) + + const {mutate: createGroupChat} = useCreateGroupChat({ + onSuccess: data => { + onSelectChat(data.convo.id) + + ax.metric('groupchat:create', {logContext: 'SendViaChatDialog'}) + }, + onError: error => { + logger.error('Failed to share post to group chat', {safeMessage: error}) + let errorMessage = l`An issue occurred starting the group chat, please try again.` + if (isNetworkError(error)) { + errorMessage = l`A network error occurred. Please check your internet connection.` + } else if ( + error instanceof ChatBskyGroupCreateGroup.AccountSuspendedError + ) { + errorMessage = l`Suspended accounts cannot participate in a group chat.` + } else if (error instanceof ChatBskyGroupCreateGroup.BlockedActorError) { + errorMessage = l`One of the selected recipients has blocked you and cannot be messaged.` + } else if ( + error instanceof + ChatBskyGroupCreateGroup.NewAccountCannotCreateGroupError + ) { + errorMessage = l`You cannot create a group chat yet.` + } else if ( + error instanceof ChatBskyGroupCreateGroup.NotFollowedBySenderError + ) { + errorMessage = l`A selected recipient is not followed by the sender.` + } else if ( + error instanceof ChatBskyGroupCreateGroup.RecipientNotFoundError + ) { + errorMessage = l`Unable to find a selected recipient.` + } else if ( + error instanceof ChatBskyGroupCreateGroup.UserForbidsGroupsError + ) { + errorMessage = l`One of the selected recipients does not allow group chats.` + } + Toast.show(errorMessage, { type: 'error', }) }, @@ -67,9 +152,28 @@ function SendViaChatDialogInner({ [control, createChat], ) - return ( + const onCreateGroupChat = useCallback( + (members: string[], name: string) => { + control.close(() => { + createGroupChat({members, name}) + }) + }, + [control, createGroupChat], + ) + + return isGroupChatEnabled ? ( + + ) : ( { if (chat.kind === 'user') { onCreateChat(chat.did) From b80d09000f603d3395b9c8e9d1ee8db792ffe9cb Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:11:00 +0000 Subject: [PATCH 32/32] Nightly source-language update --- src/locale/locales/en/messages.po | 554 ++++++++++++++++-------------- 1 file changed, 296 insertions(+), 258 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 027338657d..51d10b8cf8 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -114,6 +114,7 @@ msgstr "" #. placeholder {0}: convo.details.memberCount #: src/components/dialogs/SearchablePeopleList.tsx:555 +#: src/components/dms/InitiateChatFlow.tsx:1038 msgid "{0, plural, one {# member} other {# members}}" msgstr "{0, plural, one {# member} other {# members}}" @@ -191,7 +192,10 @@ msgid "{0, plural, one {following} other {following}}" msgstr "" #. placeholder {0}: profile.postsCount || 0 +#. placeholder {0}: trend.postCount +#: src/components/interstitials/FeedTrendingTopics.tsx:223 #: src/screens/Profile/Header/Metrics.tsx:58 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:153 msgid "{0, plural, one {post} other {posts}}" msgstr "" @@ -223,6 +227,16 @@ msgstr "" msgid "{0} (Account)" msgstr "" +#. '{postCount} {posts}', e.g., '1.2K posts' +#. '{postCount} {posts}', e.g., '1.2K posts' +#. placeholder {0}: formatCount(i18n, trend.postCount) +#. placeholder {1}: import {useMemo} from 'react' import {Pressable, View} from 'react-native' import {Image} from 'expo-image' import { type AppBskyUnspeccedDefs, moderateProfile, RichText as RichTextApi, } from '@atproto/api' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useTrendingSettings} from '#/state/preferences/trending' import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery' import {useTrendingConfig} from '#/state/service-config' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {formatCount} from '#/view/com/util/numeric/format' import {atoms as a, useGutters, useTheme, type ViewStyleProp} from '#/alf' import {AvatarStack} from '#/components/AvatarStack' import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending' import {Link} from '#/components/Link' import {RichText} from '#/components/RichText' import {SubtleHover} from '#/components/SubtleHover' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import * as ModuleHeader from '../components/ModuleHeader' const TOPIC_COUNT = 5 const IMAGE_SIZE = 56 export function ExploreTrendingTopics() { const {enabled} = useTrendingConfig() const {trendingDisabled} = useTrendingSettings() return enabled && !trendingDisabled ? : null } function Inner() { const ax = useAnalytics() const {data: trending, error, isLoading, isRefetching} = useGetTrendsQuery() const noTopics = !isLoading && !error && !trending?.trends?.length const showLoading = isLoading || isRefetching if (!showLoading && (error || !trending?.trends || noTopics)) return null return ( Trending {showLoading ? Array.from({length: TOPIC_COUNT}).map((__, i) => ( )) : trending?.trends.map((trend, index) => ( { ax.metric('trendingTopic:click', { context: 'explore', recId: trending.recId, }) }} /> ))} ) } export function TrendRow({ trend, rank, children, onPress, }: ViewStyleProp & { trend: AppBskyUnspeccedDefs.TrendView rank: number children?: React.ReactNode onPress?: () => void }) { const t = useTheme() const {t: l, i18n} = useLingui() const gutters = useGutters([0, 'base']) const actors = useModerateTrendingActors(trend.actors) const description = useMemo(() => { if (!trend.description) return const rt = new RichTextApi({text: trend.description}) rt.detectFacetsWithoutResolution() return rt }, [trend.description]) let imageUrl = null // TODO Image URL goes here when available. -dsb return ( {({hovered, pressed}) => ( <> {rank}. {trend.displayName} {description ? ( ) : null} {actors.length > 0 ? ( ) : null} {trend.postCount >= 1000 ? ( 1K+ posts ) : ( {formatCount(i18n, trend.postCount)}{' '} {plural(trend.postCount, {one: 'post', other: 'posts'})} )} {imageUrl ? ( {trend.topic} ) : null} {children} )} ) } // Unused atm, but leaving here so we don't lose localization. -dsb export function useCategoryDisplayName( category: AppBskyUnspeccedDefs.TrendView['category'], ) { const {t: l} = useLingui() switch (category) { case 'sports': return l`Sports` case 'politics': return l`Politics` case 'video-games': return l`Video Games` case 'pop-culture': return l`Entertainment` case 'news': return l`News` case 'other': default: return null } } export function TrendingTopicRowSkeleton() { const t = useTheme() const gutters = useGutters([0, 'base']) return ( {/* TODO Image placeholder goes here when images are available. -dsb */} ) } function useModerateTrendingActors( actors: AppBskyUnspeccedDefs.TrendView['actors'], ) { const moderationOpts = useModerationOpts() return useMemo(() => { if (!moderationOpts) return [] return actors .filter(actor => { const decision = moderateProfile(actor, moderationOpts) return !decision.ui('avatar').filter && !decision.ui('avatar').blur }) .slice(0, 3) }, [actors, moderationOpts]) } +#. placeholder {1}: 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 {useModerationOpts} from '#/state/preferences/moderation-opts' import {useTrendingSettings} from '#/state/preferences/trending' import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery' import {useTrendingConfig} from '#/state/service-config' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {formatCount} from '#/view/com/util/numeric/format' import { atoms as a, useGutters, useLayoutBreakpoints, useTheme, type ViewStyleProp, } from '#/alf' import {alpha} from '#/alf/utils' import {AvatarStack} from '#/components/AvatarStack' import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending' import {Link} from '#/components/Link' import {SubtleHover} from '#/components/SubtleHover' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' const TOPIC_COUNT = 3 export function FeedTrendingTopicsInterstitial() { const {enabled} = useTrendingConfig() const {trendingDisabled} = useTrendingSettings() const {rightNavVisible} = useLayoutBreakpoints() return enabled && !trendingDisabled && !rightNavVisible ? : null } function Inner() { const t = useTheme() const {t: l} = useLingui() const gutters = useGutters([0, 'base']) const ax = useAnalytics() const { data: trending, error, isLoading, isRefetching, } = useGetTrendsQuery({limit: TOPIC_COUNT}) const noTopics = !isLoading && !error && !trending?.trends?.length 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 } return ( c[1]) as [string, string, ...string[]]} locations={ gradient.values.map(c => c[0]) as [number, number, ...number[]] } style={[a.absolute, a.inset_0]} /> Trending See more {isLoading || isRefetching ? Array.from({length: TOPIC_COUNT}).map((_, i) => ( )) : trending?.trends?.map((trend, index) => ( { ax.metric('trendingTopic:click', {context: 'interstitial'}) }} /> ))} ) } function TrendRow({ trend, rank, onPress, }: ViewStyleProp & { trend: AppBskyUnspeccedDefs.TrendView rank: number children?: React.ReactNode onPress?: () => void }) { const t = useTheme() const {t: l, i18n} = useLingui() const actors = useModerateTrendingActors(trend.actors) return ( {({hovered, pressed}) => ( <> {rank}. {trend.displayName} {actors.length > 0 ? ( ) : null} {trend.postCount >= 1000 ? ( 1K+ posts ) : ( {formatCount(i18n, trend.postCount)}{' '} {plural(trend.postCount, {one: 'post', other: 'posts'})} )} )} ) } function TrendingTopicRowSkeleton({rank}: {rank: number}) { const t = useTheme() return ( ) } function useModerateTrendingActors( actors: AppBskyUnspeccedDefs.TrendView['actors'], ) { const moderationOpts = useModerationOpts() return useMemo(() => { if (!moderationOpts) return [] return actors .filter(actor => { const decision = moderateProfile(actor, moderationOpts) return !decision.ui('avatar').filter && !decision.ui('avatar').blur }) .slice(0, 3) }, [actors, moderationOpts]) } +#: src/components/interstitials/FeedTrendingTopics.tsx:221 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:151 +msgid "{0} {1}" +msgstr "{0} {1}" + #. Pattern: {wordValue} in tags #. placeholder {0}: word.value #: src/components/dialogs/MutedWords.tsx:495 @@ -639,7 +653,7 @@ msgid "{following} following" msgstr "" #: src/components/dms/components/GroupChatProfileCard.tsx:62 -#: src/components/dms/InitiateChatFlow.tsx:945 +#: src/components/dms/InitiateChatFlow.tsx:1158 msgid "{handle} can’t be added" msgstr "{handle} can’t be added" @@ -647,7 +661,7 @@ msgstr "{handle} can’t be added" msgid "{handle} can't be messaged" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:906 +#: src/components/dms/InitiateChatFlow.tsx:1119 msgid "{handle} can’t be messaged" msgstr "{handle} can’t be messaged" @@ -734,7 +748,9 @@ msgid "{profileName} joined Bluesky using a starter pack {timeAgoString} ago" msgstr "" #. The trending topic rank, i.e. "1. March Madness", "2. The Bachelor" -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:101 +#. The trending topic rank, i.e. "1. March Madness", "2. The Bachelor" +#: src/components/interstitials/FeedTrendingTopics.tsx:203 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:123 msgid "{rank}." msgstr "" @@ -764,11 +780,6 @@ msgstr "{requestCount, plural, one {# request} other {# requests}}" msgid "{requestCount}+ requests" msgstr "{requestCount}+ requests" -#. trending topic time spent trending. should be as short as possible to fit in a pill -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:191 -msgid "{type}h ago" -msgstr "" - #: src/components/verification/VerifierDialog.tsx:62 msgid "{userName} is a trusted verifier" msgstr "" @@ -874,7 +885,7 @@ msgid "<0>{displayName}<1/><2> added you" msgstr "<0>{displayName}<1/><2> added you" #: src/screens/Hashtag.tsx:230 -#: src/screens/Search/SearchResults.tsx:396 +#: src/screens/Search/SearchResults.tsx:412 msgid "<0>Sign in<1> or <2>create an account<3> <4>to search for news, sports, politics, and everything else happening on Bluesky." msgstr "" @@ -892,6 +903,13 @@ msgstr "" msgid "⚠Invalid Handle" msgstr "" +#. Over 1,000 posts +#. Over 1,000 posts +#: src/components/interstitials/FeedTrendingTopics.tsx:219 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:149 +msgid "1K+ posts" +msgstr "1K+ posts" + #: src/components/dialogs/MutedWords.tsx:202 #: src/components/dialogs/MutedWords.tsx:551 #: src/components/dialogs/MutedWords.tsx:554 @@ -931,9 +949,11 @@ msgstr "" #: src/components/contacts/screens/VerifyNumber.tsx:155 #: src/components/dms/dialogs/NewChatDialog.tsx:56 #: src/components/dms/dialogs/NewChatDialog.tsx:94 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:71 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:110 #: src/components/dms/LeaveConvoPrompt.tsx:38 -#: src/components/moderation/BlockDialog.tsx:287 -#: src/components/moderation/BlockDialog.tsx:313 +#: src/components/moderation/BlockDialog.tsx:284 +#: src/components/moderation/BlockDialog.tsx:310 #: src/screens/Messages/JoinRequests.tsx:192 #: src/screens/Messages/JoinRequests.tsx:225 msgid "A network error occurred. Please check your internet connection." @@ -971,6 +991,7 @@ msgid "A screenshot of the post composer with a new button next to the post butt msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:109 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:125 msgid "A selected recipient is not followed by the sender." msgstr "A selected recipient is not followed by the sender." @@ -1178,7 +1199,7 @@ msgstr "" msgid "Add another post to thread" msgstr "" -#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:435 +#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:439 msgid "Add another search filter" msgstr "Add another search filter" @@ -1200,7 +1221,7 @@ msgstr "Add automation label to account" msgid "Add emoji reaction" msgstr "" -#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:443 +#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:447 msgid "Add filter" msgstr "Add filter" @@ -1652,17 +1673,15 @@ msgstr "An invite link lets people join this group chat without being added dire msgid "An issue not included in these options" msgstr "" -#: src/components/dms/dialogs/NewChatDialog.tsx:92 -msgid "An issue occurred creating the group chat, please try again." -msgstr "An issue occurred creating the group chat, please try again." - #: src/components/dms/dialogs/NewChatDialog.tsx:54 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:69 msgid "An issue occurred starting the chat, please try again." msgstr "An issue occurred starting the chat, please try again." -#: src/components/dms/dialogs/ShareViaChatDialog.tsx:50 -msgid "An issue occurred while trying to open the chat" -msgstr "" +#: src/components/dms/dialogs/NewChatDialog.tsx:92 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:108 +msgid "An issue occurred starting the group chat, please try again." +msgstr "An issue occurred starting the group chat, please try again." #: src/components/hooks/useFollowMethods.ts:35 #: src/components/hooks/useFollowMethods.ts:52 @@ -1716,8 +1735,8 @@ msgid "Any date" msgstr "Any date" #: src/components/dialogs/PostInteractionSettingsDialog.tsx:457 -#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:22 -#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:25 +#: src/screens/Search/components/AdvancedSearchDialog/FromDropdown.tsx:22 +#: src/screens/Search/components/AdvancedSearchDialog/FromDropdown.tsx:26 msgid "Anyone" msgstr "" @@ -1966,7 +1985,7 @@ msgid "Aurora" msgstr "" #: src/components/BotAccountAlert.tsx:32 -#: src/components/BotBadge.tsx:63 +#: src/components/BotBadge.tsx:65 msgid "Automated account" msgstr "Automated account" @@ -2000,9 +2019,9 @@ msgstr "" #: src/components/dms/AddMembersFlow.tsx:359 #: src/components/dms/AddMembersFlow.tsx:527 #: src/components/dms/AddMembersFlow.tsx:533 -#: src/components/dms/InitiateChatFlow.tsx:540 -#: src/components/dms/InitiateChatFlow.tsx:758 -#: src/components/dms/InitiateChatFlow.tsx:765 +#: src/components/dms/InitiateChatFlow.tsx:645 +#: src/components/dms/InitiateChatFlow.tsx:863 +#: src/components/dms/InitiateChatFlow.tsx:870 #: src/components/moderation/AppealForm.tsx:145 #: src/components/moderation/AppealForm.tsx:146 #: src/screens/Login/ChooseAccountForm.tsx:96 @@ -2114,6 +2133,12 @@ msgstr "" msgid "Beta features" msgstr "Beta features" +#: src/components/BetaBadge.tsx:79 +#: src/components/BetaBadge.tsx:99 +#: src/components/BetaBadge.tsx:100 +msgid "Beta features enabled" +msgstr "Beta features enabled" + #: src/screens/Settings/BetaFeaturesSettings.tsx:144 msgctxt "web" msgid "Beta features may be unstable. Some changes may require reloading the app." @@ -2132,9 +2157,9 @@ msgstr "" msgid "Birthday" msgstr "" -#: src/components/moderation/BlockDialog.tsx:186 -#: src/components/moderation/BlockDialog.tsx:192 -#: src/components/moderation/BlockDialog.tsx:211 +#: src/components/moderation/BlockDialog.tsx:187 +#: src/components/moderation/BlockDialog.tsx:193 +#: src/components/moderation/BlockDialog.tsx:213 #: src/screens/Messages/ConversationSettings/MemberMenu.tsx:227 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190 msgid "Block" @@ -2252,6 +2277,7 @@ msgstr "bloomscrolling booksky" #: src/components/dialogs/ServerInput.tsx:144 #: src/components/dialogs/ServerInput.tsx:146 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:514 msgid "Bluesky" msgstr "" @@ -2372,24 +2398,25 @@ msgstr "" msgid "Browse other feeds" msgstr "" -#: src/components/TrendingTopics.tsx:179 +#: src/components/TrendingTopics.tsx:58 msgid "Browse posts about {displayName}" msgstr "" -#: src/components/TrendingTopics.tsx:187 +#: src/components/TrendingTopics.tsx:66 msgid "Browse posts tagged with {displayName}" msgstr "" -#: src/components/TrendingTopics.tsx:196 +#: src/components/TrendingTopics.tsx:75 msgid "Browse starter pack {displayName}" msgstr "" #. placeholder {0}: trend.displayName -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:83 +#: src/components/interstitials/FeedTrendingTopics.tsx:170 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:104 msgid "Browse topic {0}" msgstr "" -#: src/components/TrendingTopics.tsx:233 +#: src/components/TrendingTopics.tsx:112 msgid "Browse topic {displayName}" msgstr "" @@ -2409,7 +2436,7 @@ msgstr "" #: src/components/moderation/ReportDialog/index.tsx:847 #: src/screens/Messages/JoinRequest.tsx:177 #: src/screens/Search/components/StarterPackCard.tsx:112 -#: src/screens/Search/Explore.tsx:971 +#: src/screens/Search/Explore.tsx:960 msgid "By {0}" msgstr "" @@ -2420,9 +2447,9 @@ msgstr "by @{0}" #. The group chat creator, in the format 'By {displayName}'. #. placeholder {0}: createSanitizedDisplayName( joinLinkPreview.owner, true, moderateProfile(joinLinkPreview.owner, moderationOpts).ui( 'displayName', ), ) -#. placeholder {0}: sanitizeHandle(info.creatorHandle, '@') +#. placeholder {0}: info.creatorHandle === TRENDING_HANDLE ? l`Bluesky` : sanitizeHandle(info.creatorHandle, '@') #: src/components/intents/GroupChatJoinDialog.tsx:379 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:451 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:502 msgid "By <0>{0}" msgstr "" @@ -2481,7 +2508,7 @@ msgstr "Camera access needed" #: src/components/dialogs/nuxs/InviteFriendsAnnouncement.tsx:175 #: src/components/dialogs/nuxs/InviteFriendsAnnouncement.tsx:181 #: src/components/Menu/index.tsx:373 -#: src/components/moderation/BlockDialog.tsx:202 +#: src/components/moderation/BlockDialog.tsx:204 #: src/components/PostControls/RepostButton.tsx:210 #: src/components/Prompt.tsx:152 #: src/components/Prompt.tsx:154 @@ -2532,7 +2559,7 @@ msgstr "" msgid "Cancel reply" msgstr "Cancel reply" -#: src/screens/Search/Shell.tsx:584 +#: src/screens/Search/Shell.tsx:592 msgid "Cancel search" msgstr "" @@ -2685,8 +2712,8 @@ msgctxt "toast" msgid "Chat muted" msgstr "" -#: src/components/moderation/BlockDialog.tsx:289 -#: src/components/moderation/BlockDialog.tsx:317 +#: src/components/moderation/BlockDialog.tsx:286 +#: src/components/moderation/BlockDialog.tsx:314 msgid "Chat not found." msgstr "Chat not found." @@ -2696,11 +2723,12 @@ msgstr "Chat not found." msgid "Chat options" msgstr "Chat options" -#: src/components/moderation/BlockDialog.tsx:293 +#: src/components/moderation/BlockDialog.tsx:290 msgid "Chat owners cannot leave a group chat." msgstr "Chat owners cannot leave a group chat." #: src/components/dms/dialogs/NewChatDialog.tsx:73 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:88 msgid "Chat recipient is not followed by the sender." msgstr "Chat recipient is not followed by the sender." @@ -2950,7 +2978,7 @@ msgstr "" #: src/components/dms/AfterReportDialog.tsx:212 #: src/components/dms/AfterReportDialog.tsx:217 #: src/components/dms/EmojiPopup.android.tsx:59 -#: src/components/dms/InitiateChatFlow.tsx:565 +#: src/components/dms/InitiateChatFlow.tsx:670 #: src/components/intents/GroupChatJoinDialog.tsx:246 #: src/components/intents/GroupChatJoinDialog.tsx:281 #: src/components/NewskieDialog.tsx:169 @@ -2994,7 +3022,7 @@ msgstr "Close banner" #: src/components/dialogs/LanguageSelectDialog.tsx:322 #: src/components/dialogs/LanguageSelectDialog.tsx:354 #: src/components/dialogs/NotificationSettingsDialog.tsx:102 -#: src/components/moderation/BlockDialog.tsx:199 +#: src/components/moderation/BlockDialog.tsx:201 #: src/components/verification/VerificationsDialog.tsx:138 #: src/components/verification/VerifierDialog.tsx:140 #: src/features/gifPicker/components/GifPickerErrorBoundary.tsx:36 @@ -3212,10 +3240,6 @@ msgstr "" msgid "Content filters" msgstr "" -#: src/screens/Search/modules/ExploreRecommendations.tsx:60 -msgid "Content from across the network we think you might like." -msgstr "" - #: src/screens/Settings/LanguageSettings.tsx:177 msgid "Content languages" msgstr "" @@ -3273,7 +3297,7 @@ msgid "Continue thread..." msgstr "" #: src/components/dms/AddMembersFlow.tsx:324 -#: src/components/dms/InitiateChatFlow.tsx:493 +#: src/components/dms/InitiateChatFlow.tsx:598 msgid "Continue to group name" msgstr "Continue to group name" @@ -3315,7 +3339,7 @@ msgstr "Conversation not found." msgid "Copied build version to clipboard" msgstr "" -#: src/screens/Search/Shell.tsx:504 +#: src/screens/Search/Shell.tsx:512 msgid "Copied link to clipboard" msgstr "Copied link to clipboard" @@ -3323,7 +3347,7 @@ msgstr "Copied link to clipboard" #: src/components/dms/MessageContextMenu.tsx:97 #: src/components/PostControls/DiscoverDebug.tsx:36 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:273 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:77 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:75 #: src/lib/sharing.ts:24 #: src/lib/sharing.ts:42 msgid "Copied to clipboard" @@ -3357,8 +3381,8 @@ msgstr "" msgid "Copy at:// URI" msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:154 -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:157 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:152 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:155 msgid "Copy author DID" msgstr "" @@ -3396,10 +3420,10 @@ msgstr "" msgid "Copy link to list" msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:142 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:145 -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:88 -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:91 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:140 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:143 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:86 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:89 msgid "Copy link to post" msgstr "" @@ -3417,8 +3441,8 @@ msgstr "" msgid "Copy message text" msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:145 -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:148 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:143 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:146 msgid "Copy post at:// URI" msgstr "" @@ -3487,11 +3511,11 @@ msgstr "" msgid "Could not leave chat" msgstr "" -#: src/components/moderation/BlockDialog.tsx:285 +#: src/components/moderation/BlockDialog.tsx:282 msgid "Could not leave chat." msgstr "Could not leave chat." -#: src/screens/Profile/ProfileFeed/index.tsx:73 +#: src/screens/Profile/ProfileFeed/index.tsx:72 msgid "Could not load feed" msgstr "" @@ -3509,7 +3533,7 @@ msgstr "Could not load profile" msgid "Could not mute chat" msgstr "" -#: src/components/moderation/BlockDialog.tsx:311 +#: src/components/moderation/BlockDialog.tsx:308 msgid "Could not remove member." msgstr "Could not remove member." @@ -3553,7 +3577,7 @@ msgstr "cows pigs" #. Text on button to create a new starter pack #: src/components/dialogs/StarterPackDialog.tsx:113 #: src/components/dialogs/StarterPackDialog.tsx:210 -#: src/components/dms/InitiateChatFlow.tsx:502 +#: src/components/dms/InitiateChatFlow.tsx:607 #: src/components/StarterPack/ProfileStarterPacks.tsx:332 msgid "Create" msgstr "" @@ -3604,7 +3628,7 @@ msgstr "" #: src/components/dialogs/Signin.tsx:87 #: src/components/dialogs/Signin.tsx:89 #: src/screens/Hashtag.tsx:236 -#: src/screens/Search/SearchResults.tsx:402 +#: src/screens/Search/SearchResults.tsx:418 msgid "Create an account" msgstr "" @@ -3621,7 +3645,7 @@ msgstr "" msgid "Create another" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:501 +#: src/components/dms/InitiateChatFlow.tsx:606 msgid "Create group chat" msgstr "Create group chat" @@ -3690,7 +3714,7 @@ msgstr "" msgid "Current beta features" msgstr "Current beta features" -#: src/components/moderation/BlockDialog.tsx:378 +#: src/components/moderation/BlockDialog.tsx:375 msgid "Current chat" msgstr "Current chat" @@ -4026,6 +4050,10 @@ msgstr "" msgid "Discourage apps from showing my account to logged-out users" msgstr "" +#: src/screens/Search/Explore.tsx:448 +msgid "Discover feeds" +msgstr "Discover feeds" + #: src/view/com/posts/FollowingEmptyState.tsx:64 #: src/view/com/posts/FollowingEmptyState.tsx:69 #: src/view/com/posts/FollowingEndOfFeed.tsx:65 @@ -4033,10 +4061,6 @@ msgstr "" msgid "Discover new custom feeds" msgstr "" -#: src/screens/Search/Explore.tsx:453 -msgid "Discover new feeds" -msgstr "" - #: src/view/screens/Feeds.tsx:723 msgid "Discover New Feeds" msgstr "" @@ -4058,11 +4082,11 @@ msgstr "" msgid "Dismiss getting started guide" msgstr "" -#: src/screens/Search/modules/ExploreInterestsCard.tsx:44 +#: src/screens/Search/modules/ExploreInterestsCard.tsx:42 msgid "Dismiss interests" msgstr "" -#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:82 +#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:74 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:64 msgid "Dismiss live event banner" msgstr "" @@ -4313,8 +4337,8 @@ msgstr "" msgid "Edit interaction settings" msgstr "" -#: src/screens/Search/modules/ExploreInterestsCard.tsx:99 -#: src/screens/Search/modules/ExploreInterestsCard.tsx:106 +#: src/screens/Search/modules/ExploreInterestsCard.tsx:93 +#: src/screens/Search/modules/ExploreInterestsCard.tsx:100 msgid "Edit interests" msgstr "" @@ -4453,8 +4477,8 @@ msgstr "" #: src/components/dialogs/Embed.tsx:105 #: src/components/dialogs/Embed.tsx:109 -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:120 -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:125 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:118 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:123 msgid "Embed post" msgstr "" @@ -4604,7 +4628,7 @@ msgstr "" msgid "Enters full screen" msgstr "" -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:224 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:202 msgid "Entertainment" msgstr "" @@ -4638,7 +4662,7 @@ msgstr "" msgid "Error receiving captcha response." msgstr "" -#: src/screens/Search/SearchResults.tsx:190 +#: src/screens/Search/SearchResults.tsx:206 msgid "Error: {error}" msgstr "" @@ -4650,8 +4674,8 @@ msgstr "" msgid "Everybody can reply to this post." msgstr "" -#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:168 -#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:171 +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:170 +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:173 msgid "Everyone" msgstr "" @@ -4757,7 +4781,7 @@ msgid "Explicit sexual images." msgstr "" #: src/Navigation.tsx:769 -#: src/screens/Search/Shell.tsx:541 +#: src/screens/Search/Shell.tsx:549 #: src/view/shell/desktop/LeftNav.tsx:677 #: src/view/shell/Drawer.tsx:473 msgid "Explore" @@ -4840,7 +4864,7 @@ msgstr "" #: src/components/Lightbox/Lightbox.web.tsx:302 #: src/features/inviteFriends/InviteFriendsDialogInner.tsx:130 -#: src/screens/Search/Shell.tsx:505 +#: src/screens/Search/Shell.tsx:513 msgid "Failed to copy link" msgstr "Failed to copy link" @@ -4933,16 +4957,16 @@ msgstr "Failed to leave group chat" msgid "Failed to load conversations" msgstr "" -#: src/screens/Search/Explore.tsx:530 -#: src/screens/Search/Explore.tsx:575 -#: src/screens/Search/Explore.tsx:621 +#: src/screens/Search/Explore.tsx:526 +#: src/screens/Search/Explore.tsx:571 +#: src/screens/Search/Explore.tsx:617 msgid "Failed to load feeds" msgstr "" -#: src/screens/Search/Explore.tsx:489 -#: src/screens/Search/Explore.tsx:544 -#: src/screens/Search/Explore.tsx:589 -#: src/screens/Search/Explore.tsx:635 +#: src/screens/Search/Explore.tsx:485 +#: src/screens/Search/Explore.tsx:540 +#: src/screens/Search/Explore.tsx:585 +#: src/screens/Search/Explore.tsx:631 msgid "Failed to load feeds preferences" msgstr "" @@ -4967,14 +4991,14 @@ msgstr "" msgid "Failed to load profiles" msgstr "Failed to load profiles" -#: src/screens/Search/Explore.tsx:482 -#: src/screens/Search/Explore.tsx:537 -#: src/screens/Search/Explore.tsx:582 -#: src/screens/Search/Explore.tsx:628 +#: src/screens/Search/Explore.tsx:478 +#: src/screens/Search/Explore.tsx:533 +#: src/screens/Search/Explore.tsx:578 +#: src/screens/Search/Explore.tsx:624 msgid "Failed to load suggested feeds" msgstr "" -#: src/screens/Search/Explore.tsx:391 +#: src/screens/Search/Explore.tsx:386 msgid "Failed to load suggested follows" msgstr "" @@ -5167,7 +5191,7 @@ msgstr "" msgid "Feed identifier" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:353 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:401 msgid "Feed menu" msgstr "" @@ -5194,7 +5218,7 @@ msgstr "" #: src/Navigation.tsx:545 #: src/screens/SavedFeeds.tsx:112 #: src/screens/SavedFeeds.tsx:303 -#: src/screens/Search/SearchResults.tsx:106 +#: src/screens/Search/SearchResults.tsx:113 #: src/screens/StarterPack/StarterPackScreen.tsx:196 #: src/view/screens/Feeds.tsx:504 #: src/view/screens/Profile.tsx:239 @@ -5215,10 +5239,6 @@ msgctxt "toast" msgid "Feeds updated!" msgstr "" -#: src/screens/Search/modules/ExploreRecommendations.tsx:66 -msgid "Feeds we think you might like." -msgstr "" - #: src/screens/Settings/components/OTAInfo.tsx:61 msgid "Fetch update" msgstr "" @@ -5228,11 +5248,11 @@ msgstr "" msgid "File saved successfully!" msgstr "" -#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:48 +#: src/screens/Search/components/AdvancedSearchDialog/FromDropdown.tsx:49 msgid "Filter by author" msgstr "Filter by author" -#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:29 +#: src/screens/Search/components/AdvancedSearchDialog/FromDropdown.tsx:30 msgid "Filter by author (currently: {currentLabel})" msgstr "Filter by author (currently: {currentLabel})" @@ -5265,7 +5285,7 @@ msgstr "Filter this search by {0}" msgid "Filter who can opt to receive notifications for your activity" msgstr "" -#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:163 +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:165 msgid "Filter who you receive notifications from" msgstr "" @@ -5329,7 +5349,7 @@ msgstr "" msgid "Find people you know" msgstr "Find people you know" -#: src/screens/Search/Shell.tsx:753 +#: src/screens/Search/Shell.tsx:765 msgid "Find posts, users, and feeds on Bluesky" msgstr "" @@ -5606,7 +5626,9 @@ msgstr "Four message bubbles representing a group chat. First message: \"Did you msgid "Free your feed" msgstr "" -#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:406 +#. Filter search results by a specific post author +#. Filter who you receive notifications from +#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:408 #: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:159 msgid "From" msgstr "" @@ -5965,16 +5987,17 @@ msgid "Group chats can only have a maximum of {0, plural, other {# people}}." msgstr "Group chats can only have a maximum of {0, plural, other {# people}}." #: src/components/dialogs/SearchablePeopleList.tsx:565 +#: src/components/dms/InitiateChatFlow.tsx:1048 msgid "Group is locked" msgstr "Group is locked" -#: src/components/dms/InitiateChatFlow.tsx:264 -#: src/components/dms/InitiateChatFlow.tsx:600 +#: src/components/dms/InitiateChatFlow.tsx:292 +#: src/components/dms/InitiateChatFlow.tsx:705 #: src/screens/Messages/ConversationSettings/prompts.tsx:50 msgid "Group name" msgstr "Group name" -#: src/components/dms/InitiateChatFlow.tsx:625 +#: src/components/dms/InitiateChatFlow.tsx:730 #: src/screens/Messages/ConversationSettings/prompts.tsx:69 msgid "Group name is too long. {MAX_GROUP_NAME_GRAPHEME_LENGTH, plural, other {The maximum number of characters is #.}}" msgstr "Group name is too long. {MAX_GROUP_NAME_GRAPHEME_LENGTH, plural, other {The maximum number of characters is #.}}" @@ -6103,7 +6126,7 @@ msgstr "" msgid "Hidden list" msgstr "" -#: src/components/interstitials/Trending.tsx:133 +#: src/components/interstitials/Trending.tsx:143 #: src/components/interstitials/TrendingVideos.tsx:139 #: src/components/moderation/ContentHider.tsx:217 #: src/components/moderation/LabelPreference.tsx:141 @@ -6113,7 +6136,7 @@ msgstr "" #: src/lib/moderation/useLabelBehaviorDescription.ts:23 #: src/lib/moderation/useLabelBehaviorDescription.ts:28 #: src/lib/moderation/useLabelBehaviorDescription.ts:33 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:128 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:137 msgid "Hide" msgstr "" @@ -6153,7 +6176,7 @@ msgstr "" msgid "Hide reply for me" msgstr "" -#: src/screens/Search/modules/ExploreInterestsCard.tsx:111 +#: src/screens/Search/modules/ExploreInterestsCard.tsx:105 msgid "Hide this card" msgstr "" @@ -6173,12 +6196,12 @@ msgstr "" msgid "Hide translation" msgstr "Hide translation" -#: src/components/interstitials/Trending.tsx:115 +#: src/components/interstitials/Trending.tsx:125 msgid "Hide trending topics" msgstr "" -#: src/components/interstitials/Trending.tsx:131 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:126 +#: src/components/interstitials/Trending.tsx:141 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:135 msgid "Hide trending topics?" msgstr "" @@ -6276,10 +6299,6 @@ msgstr "Hosting provider: {0}" msgid "Hosting provider: Bluesky" msgstr "Hosting provider: Bluesky" -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:179 -msgid "Hot" -msgstr "" - #: src/components/contacts/components/InviteInfo.tsx:54 msgid "How it works:" msgstr "" @@ -6516,6 +6535,7 @@ msgstr "" #. Advanced search filter #. Advanced search filter #. Advanced search filter +#. Include search results with or without replies #: src/screens/Search/components/AdvancedSearchDialog/FilterBlock.tsx:47 #: src/screens/Search/components/AdvancedSearchDialog/FilterBlock.tsx:58 #: src/screens/Search/components/AdvancedSearchDialog/FilterBlock.tsx:75 @@ -6845,7 +6865,7 @@ msgid "Last initiated just now" msgstr "" #: src/screens/Hashtag.tsx:97 -#: src/screens/Search/SearchResults.tsx:88 +#: src/screens/Search/SearchResults.tsx:95 #: src/screens/Topic.tsx:64 msgid "Latest" msgstr "" @@ -6864,7 +6884,7 @@ msgstr "" msgid "Learn More" msgstr "" -#: src/screens/Search/SearchResults.tsx:257 +#: src/screens/Search/SearchResults.tsx:273 msgctxt "english-only-resource" msgid "Learn more about <0>how to use advanced search." msgstr "Learn more about <0>how to use advanced search." @@ -6949,8 +6969,8 @@ msgstr "Leave" #: src/components/dms/MessagesListBlockedFooter.tsx:109 #: src/components/dms/MessagesListBlockedFooter.tsx:116 -#: src/components/moderation/BlockDialog.tsx:384 -#: src/components/moderation/BlockDialog.tsx:391 +#: src/components/moderation/BlockDialog.tsx:381 +#: src/components/moderation/BlockDialog.tsx:388 #: src/screens/Messages/components/ChatEnded.tsx:66 #: src/screens/Messages/components/ChatLocked.tsx:112 msgid "Leave chat" @@ -6990,7 +7010,7 @@ msgstr "" msgid "Leaving this chat will lock it permanently and you won’t be able to rejoin." msgstr "Leaving this chat will lock it permanently and you won’t be able to rejoin." -#: src/components/moderation/BlockDialog.tsx:277 +#: src/components/moderation/BlockDialog.tsx:274 msgid "Left group chat." msgstr "Left group chat." @@ -7022,7 +7042,7 @@ msgctxt "Name of app icon variant" msgid "Light" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:509 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:564 msgid "Like" msgstr "" @@ -7041,7 +7061,7 @@ msgstr "" msgid "Like 10 posts to train the Discover feed" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:497 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:552 msgid "Like this feed" msgstr "" @@ -7086,7 +7106,7 @@ msgid "Liked by {0} and {1}" msgstr "Liked by {0} and {1}" #: src/components/LabelingServiceCard/index.tsx:96 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:486 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:540 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:169 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:183 msgid "Liked by {likeCount, plural, one {# user} other {# users}}" @@ -7232,12 +7252,12 @@ msgstr "" msgid "Live event happening now: {0}" msgstr "" -#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:51 +#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:43 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:33 msgid "Live event hidden" msgstr "" -#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:53 +#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:45 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:35 msgid "Live event unhidden" msgstr "" @@ -7253,12 +7273,12 @@ msgstr "" msgid "Live link" msgstr "" -#: src/screens/Search/Explore.tsx:90 +#: src/screens/Search/Explore.tsx:87 msgid "Load more" msgstr "" -#: src/screens/Search/Explore.tsx:519 -#: src/screens/Search/Explore.tsx:610 +#: src/screens/Search/Explore.tsx:515 +#: src/screens/Search/Explore.tsx:606 msgid "Load more suggested feeds" msgstr "" @@ -7266,7 +7286,7 @@ msgstr "" msgid "Load new notifications" msgstr "" -#: src/screens/Profile/ProfileFeed/index.tsx:206 +#: src/screens/Profile/ProfileFeed/index.tsx:208 #: src/screens/Profile/Sections/Feed.tsx:118 #: src/screens/ProfileList/FeedSection.tsx:113 #: src/view/com/feeds/FeedPage.tsx:168 @@ -7430,6 +7450,10 @@ msgstr "Marked all requests as read" msgid "Maybe later" msgstr "" +#: src/screens/Search/components/AdvancedSearchDialog/FromDropdown.tsx:24 +msgid "Me" +msgstr "Me" + #: src/screens/Search/components/AdvancedSearchDialog/index.tsx:375 #: src/view/screens/Profile.tsx:236 msgid "Media" @@ -7449,7 +7473,7 @@ msgstr "" msgid "Media that may be disturbing or inappropriate for some audiences." msgstr "" -#: src/components/moderation/BlockDialog.tsx:303 +#: src/components/moderation/BlockDialog.tsx:300 msgid "Member removed from group chat." msgstr "Member removed from group chat." @@ -7841,7 +7865,6 @@ msgstr "" msgid "Nevermind, create a handle for me" msgstr "" -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:184 #: src/view/com/profile/ProfileMenu.tsx:398 msgid "New" msgstr "" @@ -7910,20 +7933,20 @@ msgstr "" msgid "New followers" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:249 -#: src/components/dms/InitiateChatFlow.tsx:263 +#: src/components/dms/InitiateChatFlow.tsx:277 +#: src/components/dms/InitiateChatFlow.tsx:291 msgid "New group chat" msgstr "New group chat" #. Button used to create a new group chat. #. Button used to create a new group chat. -#: src/components/dms/InitiateChatFlow.tsx:800 -#: src/components/dms/InitiateChatFlow.tsx:834 +#: src/components/dms/InitiateChatFlow.tsx:905 +#: src/components/dms/InitiateChatFlow.tsx:939 msgctxt "action" msgid "New group chat" msgstr "New group chat" -#: src/components/dms/InitiateChatFlow.tsx:300 +#: src/components/dms/InitiateChatFlow.tsx:332 msgid "New group chat with:" msgstr "New group chat with:" @@ -7956,7 +7979,7 @@ msgstr "New messages" msgid "New password" msgstr "" -#: src/screens/Profile/ProfileFeed/index.tsx:217 +#: src/screens/Profile/ProfileFeed/index.tsx:218 #: src/screens/ProfileList/index.tsx:237 #: src/screens/ProfileList/index.tsx:280 #: src/view/screens/Feeds.tsx:545 @@ -7999,14 +8022,14 @@ msgid "Newest replies first" msgstr "" #: src/lib/interests.ts:67 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:226 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:204 msgid "News" msgstr "" #: src/components/contacts/screens/ViewMatches.tsx:395 #: src/components/contacts/screens/ViewMatches.tsx:410 #: src/components/dms/AddMembersFlow.tsx:325 -#: src/components/dms/InitiateChatFlow.tsx:494 +#: src/components/dms/InitiateChatFlow.tsx:599 #: src/screens/Login/ForgotPasswordForm.tsx:143 #: src/screens/Login/ForgotPasswordForm.tsx:150 #: src/screens/Login/SetNewPasswordForm.tsx:171 @@ -8191,13 +8214,13 @@ msgstr "" #: src/components/dialogs/SearchablePeopleList.tsx:250 #: src/components/dms/AddMembersFlow.tsx:257 -#: src/components/dms/InitiateChatFlow.tsx:376 +#: src/components/dms/InitiateChatFlow.tsx:469 #: src/components/ProgressGuide/FollowDialog.tsx:221 msgid "No results" msgstr "" #. placeholder {0}: interestsDisplayNames[selectedInterest] -#: src/screens/Search/Explore.tsx:828 +#: src/screens/Search/Explore.tsx:817 msgid "No results for \"{0}\"." msgstr "" @@ -8210,19 +8233,19 @@ msgstr "" msgid "No results found for \"{query}\"" msgstr "" -#: src/screens/Search/SearchResults.tsx:217 +#: src/screens/Search/SearchResults.tsx:233 msgid "No results found for β€œ<0>{query}” with advanced search filters applied." msgstr "No results found for β€œ<0>{query}” with advanced search filters applied." -#: src/screens/Search/SearchResults.tsx:229 +#: src/screens/Search/SearchResults.tsx:245 msgid "No results found for β€œ<0>{query}”." msgstr "No results found for β€œ<0>{query}”." -#: src/screens/Search/SearchResults.tsx:223 +#: src/screens/Search/SearchResults.tsx:239 msgid "No results found for your query with advanced search filters applied." msgstr "No results found for your query with advanced search filters applied." -#: src/screens/Search/Explore.tsx:832 +#: src/screens/Search/Explore.tsx:821 msgid "No results." msgstr "" @@ -8311,7 +8334,7 @@ msgstr "" msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:135 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:133 msgid "Note: This post is only visible to logged-in users." msgstr "" @@ -8375,7 +8398,7 @@ msgstr "" #. Confirm button text. #: src/components/contacts/screens/GetContacts.tsx:317 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:227 -#: src/screens/Search/modules/ExploreInterestsCard.tsx:49 +#: src/screens/Search/modules/ExploreInterestsCard.tsx:44 #: src/screens/Settings/AppIconSettings/index.tsx:46 #: src/screens/Settings/AppIconSettings/index.tsx:232 msgid "OK" @@ -8383,7 +8406,7 @@ msgstr "" #: src/components/BotAccountAlert.tsx:52 #: src/components/BotAccountAlert.tsx:57 -#: src/components/dms/InitiateChatFlow.tsx:733 +#: src/components/dms/InitiateChatFlow.tsx:838 #: src/components/dms/MessageItem.tsx:742 #: src/screens/Login/PasswordUpdatedForm.tsx:35 #: src/screens/PostThread/components/ThreadItemAnchor.tsx:642 @@ -8410,10 +8433,12 @@ msgid "Onboarding reset" msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:117 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:133 msgid "One of the selected recipients does not allow group chats." msgstr "One of the selected recipients does not allow group chats." #: src/components/dms/dialogs/NewChatDialog.tsx:100 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:116 msgid "One of the selected recipients has blocked you and cannot be messaged." msgstr "One of the selected recipients has blocked you and cannot be messaged." @@ -8558,12 +8583,13 @@ msgstr "" msgid "Open emoji picker" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:192 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:214 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:228 msgid "Open feed info screen" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:293 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:298 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:338 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:343 msgid "Open feed options menu" msgstr "" @@ -8909,7 +8935,7 @@ msgid "Pause video" msgstr "" #: src/screens/ProfileList/index.tsx:167 -#: src/screens/Search/SearchResults.tsx:100 +#: src/screens/Search/SearchResults.tsx:107 #: src/screens/StarterPack/StarterPackScreen.tsx:195 msgid "People" msgstr "" @@ -8932,8 +8958,9 @@ msgstr "" msgid "People following @{0}" msgstr "" -#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:178 -#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:182 +#: src/screens/Search/components/AdvancedSearchDialog/FromDropdown.tsx:23 +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:180 +#: src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx:184 msgid "People I follow" msgstr "" @@ -8956,7 +8983,6 @@ msgid "People I follow can request to join" msgstr "People I follow can request to join" #: src/components/dialogs/PostInteractionSettingsDialog.tsx:510 -#: src/screens/Search/components/AdvancedSearchDialog/FollowingDropdown.tsx:23 msgid "People you follow" msgstr "" @@ -9002,8 +9028,8 @@ msgid "Pictures meant for adults." msgstr "" #: src/components/FeedCard.tsx:364 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:514 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:520 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:569 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:578 #: src/screens/SavedFeeds.tsx:559 msgid "Pin feed" msgstr "" @@ -9013,7 +9039,7 @@ msgstr "" msgid "Pin to home" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:337 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:385 msgid "Pin to Home" msgstr "" @@ -9027,8 +9053,8 @@ msgid "Pinned" msgstr "" #. placeholder {0}: info.displayName -#: src/screens/Profile/components/ProfileFeedHeader.tsx:159 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:173 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:166 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:180 msgid "Pinned {0} to Home" msgstr "" @@ -9240,7 +9266,7 @@ msgid "Please write your message below:" msgstr "" #: src/lib/interests.ts:70 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:220 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:198 msgid "Politics" msgstr "" @@ -9628,7 +9654,7 @@ msgstr "" msgid "Read {0, plural, one {# more reply} other {# more replies}}" msgstr "" -#: src/screens/Search/SearchResults.tsx:260 +#: src/screens/Search/SearchResults.tsx:276 msgctxt "english-only-resource" msgid "Read about how to use advanced search filters" msgstr "Read about how to use advanced search filters" @@ -9702,10 +9728,6 @@ msgstr "Recent searches" msgid "Recently used" msgstr "" -#: src/screens/Search/modules/ExploreRecommendations.tsx:55 -msgid "Recommended" -msgstr "" - #: src/screens/Messages/components/MessageListError.tsx:19 msgid "Reconnect" msgstr "" @@ -9826,8 +9848,8 @@ msgstr "Remove filter" msgid "Remove from chat" msgstr "Remove from chat" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:320 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:325 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:368 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:373 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:176 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:179 #: src/screens/SavedFeeds.tsx:549 @@ -9861,8 +9883,8 @@ msgstr "" msgid "Remove live status" msgstr "" -#: src/components/moderation/BlockDialog.tsx:365 -#: src/components/moderation/BlockDialog.tsx:372 +#: src/components/moderation/BlockDialog.tsx:362 +#: src/components/moderation/BlockDialog.tsx:369 msgid "Remove member" msgstr "Remove member" @@ -9933,7 +9955,7 @@ msgstr "" msgid "Removed from starter pack" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:121 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:128 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:77 #: src/view/com/posts/FeedShutdownMsg.tsx:45 msgid "Removed from your feeds" @@ -10080,8 +10102,8 @@ msgstr "" msgid "Report dialog" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:536 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:542 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:596 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:602 msgid "Report feed" msgstr "" @@ -10469,8 +10491,8 @@ msgstr "" msgid "Save these options for next time" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:320 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:326 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:368 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:374 msgid "Save to my feeds" msgstr "" @@ -10491,7 +10513,7 @@ msgstr "" msgid "Saved Posts" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:131 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:138 #: src/screens/ProfileList/components/Header.tsx:88 msgid "Saved to your feeds" msgstr "" @@ -10546,9 +10568,9 @@ msgstr "" #: src/components/forms/SearchInput.tsx:53 #: src/screens/Search/components/AdvancedSearchDialog/index.tsx:218 #: src/screens/Search/components/AdvancedSearchDialog/index.tsx:224 -#: src/screens/Search/Shell.tsx:541 -#: src/screens/Search/Shell.tsx:604 -#: src/screens/Search/Shell.tsx:741 +#: src/screens/Search/Shell.tsx:549 +#: src/screens/Search/Shell.tsx:612 +#: src/screens/Search/Shell.tsx:753 #: src/view/shell/bottom-bar/BottomBar.tsx:216 msgid "Search" msgstr "" @@ -10594,11 +10616,11 @@ msgstr "Search for {q}" msgid "Search for feeds that you want to suggest to others." msgstr "" -#: src/screens/Search/Explore.tsx:378 +#: src/screens/Search/Explore.tsx:373 msgid "Search for more accounts" msgstr "" -#: src/screens/Search/Explore.tsx:456 +#: src/screens/Search/Explore.tsx:452 msgid "Search for more feeds" msgstr "" @@ -10612,7 +10634,7 @@ msgid "Search GIFs" msgstr "" #: src/screens/Hashtag.tsx:228 -#: src/screens/Search/SearchResults.tsx:394 +#: src/screens/Search/SearchResults.tsx:410 msgid "Search is currently unavailable when logged out" msgstr "" @@ -10691,6 +10713,7 @@ msgstr "" #: src/components/FeedInterstitials.tsx:491 #: src/components/FeedInterstitials.tsx:549 +#: src/components/interstitials/FeedTrendingTopics.tsx:112 msgid "See more" msgstr "" @@ -10698,6 +10721,10 @@ msgstr "" msgid "See more suggested profiles" msgstr "" +#: src/components/interstitials/FeedTrendingTopics.tsx:103 +msgid "See more trending topics" +msgstr "See more trending topics" + #: src/view/com/profile/ProfileFollows.tsx:190 #: src/view/com/profile/ProfileFollows.tsx:191 msgid "See suggested accounts" @@ -10768,6 +10795,7 @@ msgid "Select caption file (.vtt)" msgstr "Select caption file (.vtt)" #: src/components/dialogs/SearchablePeopleList.tsx:501 +#: src/components/dms/InitiateChatFlow.tsx:984 msgid "Select chat \"{name}\"" msgstr "Select chat \"{name}\"" @@ -10808,7 +10836,7 @@ msgstr "" msgid "Select GIF \"{0}\"" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:725 +#: src/components/dms/InitiateChatFlow.tsx:830 msgid "Select group chat members" msgstr "Select group chat members" @@ -10927,7 +10955,8 @@ msgstr "" msgid "Send post to {name}" msgstr "" -#: src/components/dms/dialogs/ShareViaChatDialog.tsx:72 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:167 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:176 msgid "Send post to..." msgstr "" @@ -10945,12 +10974,12 @@ msgstr "" msgid "Send verification email" msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:116 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:122 -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:105 -#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:111 -msgid "Send via direct message" -msgstr "" +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:114 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:120 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:103 +#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:109 +msgid "Send via chat" +msgstr "Send via chat" #. placeholder {0}: i18n.date(new Date(message.sentAt), { timeStyle: 'short', }) #: src/components/dms/MessageContextMenu.tsx:175 @@ -11097,8 +11126,8 @@ msgstr "Share age range" msgid "Share anyway" msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:176 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:179 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:174 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:177 msgid "Share author DID" msgstr "" @@ -11136,8 +11165,8 @@ msgstr "" msgid "Share my profile" msgstr "Share my profile" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:167 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:170 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:165 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:168 msgid "Share post at:// URI" msgstr "" @@ -11151,12 +11180,12 @@ msgstr "Share Profile" msgid "Share QR code" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:469 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:522 msgid "Share this feed" msgstr "" -#: src/screens/Search/Shell.tsx:557 -#: src/screens/Search/Shell.tsx:626 +#: src/screens/Search/Shell.tsx:565 +#: src/screens/Search/Shell.tsx:634 msgid "Share this search" msgstr "Share this search" @@ -11168,8 +11197,8 @@ msgstr "" msgid "Share this starter pack and help people join your community on Bluesky." msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:132 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:135 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:130 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:133 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:160 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:166 #: src/screens/StarterPack/StarterPackScreen.tsx:638 @@ -11340,7 +11369,7 @@ msgstr "" #: src/screens/Login/LoginForm.tsx:560 #: src/screens/Messages/JoinRequest.tsx:290 #: src/screens/Messages/JoinRequest.tsx:296 -#: src/screens/Search/SearchResults.tsx:397 +#: src/screens/Search/SearchResults.tsx:413 #: src/view/com/auth/SplashScreen.tsx:116 #: src/view/com/auth/SplashScreen.tsx:123 #: src/view/com/auth/SplashScreen.web.tsx:124 @@ -11593,7 +11622,7 @@ msgstr "" msgid "Something went wrong. Please try again." msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:532 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:592 msgid "Something wrong? Let us know." msgstr "" @@ -11632,7 +11661,7 @@ msgid "Spam or other inauthentic behavior or deception" msgstr "" #: src/lib/interests.ts:72 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:218 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:196 msgid "Sports" msgstr "" @@ -11650,7 +11679,7 @@ msgstr "" msgid "Start a group chat" msgstr "Start a group chat" -#: src/components/dms/dialogs/NewChatDialog.tsx:191 +#: src/components/dms/dialogs/NewChatDialog.tsx:192 msgid "Start a new chat" msgstr "" @@ -11664,12 +11693,12 @@ msgstr "" msgid "Start adding people!" msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:726 +#: src/components/dms/InitiateChatFlow.tsx:831 msgid "Start chat" msgstr "Start chat" #: src/components/dialogs/SearchablePeopleList.tsx:428 -#: src/components/dms/InitiateChatFlow.tsx:874 +#: src/components/dms/InitiateChatFlow.tsx:1087 msgid "Start chat with {displayName}" msgstr "" @@ -11697,11 +11726,11 @@ msgstr "" msgid "Starter pack is invalid" msgstr "" -#: src/screens/Search/SearchResults.tsx:112 +#: src/screens/Search/SearchResults.tsx:120 msgid "Starter packs" msgstr "Starter packs" -#: src/screens/Search/Explore.tsx:665 +#: src/screens/Search/Explore.tsx:661 #: src/view/screens/Profile.tsx:240 msgid "Starter Packs" msgstr "" @@ -11836,11 +11865,11 @@ msgid "Successfully verified" msgstr "" #: src/components/dms/AddMembersFlow.tsx:243 -#: src/components/dms/InitiateChatFlow.tsx:350 +#: src/components/dms/InitiateChatFlow.tsx:432 msgid "Suggested" msgstr "Suggested" -#: src/screens/Search/Explore.tsx:375 +#: src/screens/Search/Explore.tsx:369 msgid "Suggested accounts" msgstr "" @@ -11880,10 +11909,12 @@ msgid "Support for this feature in your country has not been enabled yet! Please msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:98 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:114 msgid "Suspended accounts cannot participate in a group chat." msgstr "Suspended accounts cannot participate in a group chat." #: src/components/dms/dialogs/NewChatDialog.tsx:60 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:75 msgid "Suspended accounts cannot participate in chat." msgstr "Suspended accounts cannot participate in chat." @@ -12252,7 +12283,7 @@ msgstr "There was a problem loading GIFs. Check your connection and try again." msgid "There was a problem with your internet connection, please try again" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:177 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:184 #: src/screens/ProfileList/components/Header.tsx:91 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:79 #: src/screens/SavedFeeds.tsx:99 @@ -12260,7 +12291,7 @@ msgstr "" msgid "There was an issue contacting the server" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:416 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:467 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:101 msgid "There was an issue contacting the server, please check your internet connection and try again." msgstr "" @@ -12269,8 +12300,8 @@ msgstr "" msgid "There was an issue fetching notifications. Tap here to try again." msgstr "" -#: src/screens/Search/Explore.tsx:1027 -#: src/view/com/posts/PostFeed.tsx:778 +#: src/screens/Search/Explore.tsx:1015 +#: src/view/com/posts/PostFeed.tsx:808 msgid "There was an issue fetching posts. Tap here to try again." msgstr "" @@ -12295,7 +12326,7 @@ msgstr "" msgid "There was an issue removing this feed. Please check your internet connection and try again." msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:136 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:143 #: src/view/com/posts/FeedShutdownMsg.tsx:53 #: src/view/com/posts/FeedShutdownMsg.tsx:74 msgid "There was an issue updating your feeds, please check your internet connection and try again." @@ -12370,7 +12401,7 @@ msgstr "" msgid "These URLs" msgstr "These URLs" -#: src/components/moderation/BlockDialog.tsx:356 +#: src/components/moderation/BlockDialog.tsx:353 msgid "They own this chat" msgstr "They own this chat" @@ -12523,7 +12554,7 @@ msgid "This feed is empty! You may need to follow more users or tune your langua msgstr "" #: src/components/StarterPack/Main/PostsList.tsx:41 -#: src/screens/Profile/ProfileFeed/index.tsx:171 +#: src/screens/Profile/ProfileFeed/index.tsx:170 #: src/screens/ProfileList/FeedSection.tsx:78 msgid "This feed is empty." msgstr "" @@ -12635,7 +12666,7 @@ msgstr "" msgid "This post has an unknown type of threadgate on it. Your app may be out of date." msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:157 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:155 msgid "This post is only visible to logged-in users." msgstr "" @@ -12689,6 +12720,7 @@ msgid "This user doesn't have any followers." msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:64 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:79 msgid "This user has blocked you and cannot be messaged." msgstr "This user has blocked you and cannot be messaged." @@ -12698,6 +12730,7 @@ msgid "This user has blocked you. You cannot view their content." msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:68 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:83 msgid "This user has disabled chat and cannot be messaged." msgstr "This user has disabled chat and cannot be messaged." @@ -12832,7 +12865,7 @@ msgid "Too many contacts - you've exceeded the number of contacts you can import msgstr "" #: src/screens/Hashtag.tsx:86 -#: src/screens/Search/SearchResults.tsx:76 +#: src/screens/Search/SearchResults.tsx:83 #: src/screens/Topic.tsx:58 msgid "Top" msgstr "" @@ -12879,7 +12912,9 @@ msgstr "Translation to the same language is unavailable on your device." msgid "Tree view" msgstr "" -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:48 +#: src/components/interstitials/FeedTrendingTopics.tsx:100 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:51 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:54 msgid "Trending" msgstr "" @@ -12888,7 +12923,7 @@ msgstr "" msgid "Trending GIFs" msgstr "Trending GIFs" -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:55 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:61 msgid "Trending options" msgstr "" @@ -12904,11 +12939,11 @@ msgstr "" msgid "Trust emerges from relationships, communities, and shared context, so we’re also enabling <0>trusted verifiers: organizations that can directly issue verification." msgstr "" -#: src/screens/Search/SearchResults.tsx:244 +#: src/screens/Search/SearchResults.tsx:260 msgid "Try a different search term or remove some filters." msgstr "Try a different search term or remove some filters." -#: src/screens/Search/SearchResults.tsx:246 +#: src/screens/Search/SearchResults.tsx:262 msgid "Try a different search term." msgstr "Try a different search term." @@ -12983,10 +13018,12 @@ msgid "Unable to fetch join requests." msgstr "Unable to fetch join requests." #: src/components/dms/dialogs/NewChatDialog.tsx:113 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:129 msgid "Unable to find a selected recipient." msgstr "Unable to find a selected recipient." #: src/components/dms/dialogs/NewChatDialog.tsx:77 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:92 msgid "Unable to find the selected recipient." msgstr "Unable to find the selected recipient." @@ -13013,9 +13050,9 @@ msgstr "" #: src/components/dms/MessageItem.tsx:746 #: src/components/dms/MessagesListBlockedFooter.tsx:97 #: src/components/dms/MessagesListBlockedFooter.tsx:104 -#: src/components/moderation/BlockDialog.tsx:186 -#: src/components/moderation/BlockDialog.tsx:190 -#: src/components/moderation/BlockDialog.tsx:211 +#: src/components/moderation/BlockDialog.tsx:187 +#: src/components/moderation/BlockDialog.tsx:191 +#: src/components/moderation/BlockDialog.tsx:213 #: src/screens/Messages/ConversationSettings/MemberMenu.tsx:227 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:365 @@ -13055,8 +13092,8 @@ msgstr "" msgid "Unblock list" msgstr "" -#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:58 -#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:64 +#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:50 +#: src/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner.tsx:56 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:40 #: src/features/liveEvents/components/SidebarLiveEventFeedsBanner.tsx:46 #: src/screens/Profile/components/GermButton.tsx:186 @@ -13118,7 +13155,7 @@ msgstr "" msgid "Unlabeled, abusive, or non-consensual adult content" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:509 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:564 msgid "Unlike" msgstr "" @@ -13188,14 +13225,14 @@ msgid "Unpin" msgstr "" #: src/components/FeedCard.tsx:355 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:514 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:520 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:569 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:576 #: src/screens/SavedFeeds.tsx:467 msgid "Unpin feed" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:312 -#: src/screens/Profile/components/ProfileFeedHeader.tsx:314 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:360 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:362 msgid "Unpin from home" msgstr "" @@ -13210,7 +13247,7 @@ msgid "Unpin moderation list" msgstr "" #. placeholder {0}: info.displayName -#: src/screens/Profile/components/ProfileFeedHeader.tsx:162 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:169 msgid "Unpinned {0} from Home" msgstr "" @@ -13619,7 +13656,7 @@ msgid "Video from {0}. Tap to play or pause the video" msgstr "" #: src/lib/interests.ts:62 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:222 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:200 msgid "Video Games" msgstr "" @@ -13677,7 +13714,7 @@ msgstr "" #. placeholder {0}: authors[0].profile.displayName || authors[0].profile.handle #. placeholder {0}: info.creatorHandle #. placeholder {0}: profile.handle -#: src/screens/Profile/components/ProfileFeedHeader.tsx:454 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:505 #: src/screens/Search/components/SearchProfileCard.tsx:37 #: src/screens/VideoFeed/index.tsx:880 #: src/view/com/notifications/NotificationFeedItem.tsx:619 @@ -13784,11 +13821,11 @@ msgstr "View the invite link for this group chat" msgid "View the labeling service provided by @{0}" msgstr "" -#: src/components/verification/VerificationCheckButton.tsx:93 +#: src/components/verification/VerificationCheckButton.tsx:103 msgid "View this user's verifications" msgstr "" -#: src/screens/Profile/components/ProfileFeedHeader.tsx:482 +#: src/screens/Profile/components/ProfileFeedHeader.tsx:536 msgid "View users who like this feed" msgstr "" @@ -13817,7 +13854,7 @@ msgstr "" msgid "View your muted accounts" msgstr "" -#: src/components/verification/VerificationCheckButton.tsx:92 +#: src/components/verification/VerificationCheckButton.tsx:102 msgid "View your verifications" msgstr "" @@ -14021,7 +14058,7 @@ msgid "We're having network issues, try again" msgstr "" #: src/components/dms/AddMembersFlow.tsx:197 -#: src/components/dms/InitiateChatFlow.tsx:289 +#: src/components/dms/InitiateChatFlow.tsx:321 msgid "We’re having network issues, try again" msgstr "We’re having network issues, try again" @@ -14050,15 +14087,15 @@ msgstr "" msgid "We're sorry, but we weren't able to load your muted words at this time. Please try again." msgstr "" -#: src/screens/Search/SearchResults.tsx:423 -#: src/screens/Search/SearchResults.tsx:564 -#: src/screens/Search/SearchResults.tsx:761 +#: src/screens/Search/SearchResults.tsx:439 +#: src/screens/Search/SearchResults.tsx:580 +#: src/screens/Search/SearchResults.tsx:777 msgid "We’re sorry, but your search could not be completed." msgstr "We’re sorry, but your search could not be completed." -#: src/screens/Search/SearchResults.tsx:422 -#: src/screens/Search/SearchResults.tsx:563 -#: src/screens/Search/SearchResults.tsx:760 +#: src/screens/Search/SearchResults.tsx:438 +#: src/screens/Search/SearchResults.tsx:579 +#: src/screens/Search/SearchResults.tsx:776 msgid "We’re sorry, but your search could not be completed. Please try again in a few minutes." msgstr "We’re sorry, but your search could not be completed. Please try again in a few minutes." @@ -14350,7 +14387,7 @@ msgstr "" msgid "You are verified. You will lose your verification status if you change your handle. <0>Learn more." msgstr "" -#: src/screens/Search/modules/ExploreInterestsCard.tsx:46 +#: src/screens/Search/modules/ExploreInterestsCard.tsx:43 msgid "You can adjust your interests at any time from \"Content and media\" settings." msgstr "" @@ -14427,9 +14464,9 @@ msgstr "You can read chat history but can’t send new messages." msgid "You can select up to {MAX_GALLERY_IMAGES, plural, other {# images}} in total." msgstr "You can select up to {MAX_GALLERY_IMAGES, plural, other {# images}} in total." -#: src/components/interstitials/Trending.tsx:132 +#: src/components/interstitials/Trending.tsx:142 #: src/components/interstitials/TrendingVideos.tsx:138 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:127 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:136 msgid "You can update this later from your settings." msgstr "" @@ -14439,6 +14476,7 @@ msgid "You cannot add more than {EMOJI_REACTION_LIMIT, plural, one {# emoji reac msgstr "You cannot add more than {EMOJI_REACTION_LIMIT, plural, one {# emoji reaction} other {# emoji reactions}}" #: src/components/dms/dialogs/NewChatDialog.tsx:105 +#: src/components/dms/dialogs/ShareViaChatDialog.tsx:121 msgid "You cannot create a group chat yet." msgstr "You cannot create a group chat yet." @@ -14602,7 +14640,7 @@ msgstr "" msgid "You may only add up to 3 feeds" msgstr "" -#: src/components/moderation/BlockDialog.tsx:321 +#: src/components/moderation/BlockDialog.tsx:318 msgid "You must be a chat owner to remove a member." msgstr "You must be a chat owner to remove a member." @@ -14627,7 +14665,7 @@ msgstr "" msgid "You need to verify your email address before you can enable email 2FA." msgstr "" -#: src/components/moderation/BlockDialog.tsx:352 +#: src/components/moderation/BlockDialog.tsx:349 msgid "You own this chat" msgstr "You own this chat" @@ -14787,7 +14825,7 @@ msgstr "" msgid "You've reached the maximum number of requests allowed. Please try again later." msgstr "" -#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:426 +#: src/screens/Search/components/AdvancedSearchDialog/index.tsx:430 msgid "You’ve reached the maximum of {MAX_FILTERS, plural, one {# filter} other {# filters}}. Add more values to an existing filter instead of creating new ones." msgstr "You’ve reached the maximum of {MAX_FILTERS, plural, one {# filter} other {# filters}}. Add more values to an existing filter instead of creating new ones." @@ -14823,11 +14861,11 @@ msgstr "" msgid "Your account is not yet old enough to upload videos. Please try again later." msgstr "" -#: src/components/dms/InitiateChatFlow.tsx:731 +#: src/components/dms/InitiateChatFlow.tsx:836 msgid "Your account is too new" msgstr "Your account is too new" -#: src/components/dms/InitiateChatFlow.tsx:732 +#: src/components/dms/InitiateChatFlow.tsx:837 msgid "Your account must be at least 7 days old to create a new group chat." msgstr "Your account must be at least 7 days old to create a new group chat." @@ -14919,7 +14957,7 @@ msgid "Your hosting provider is detected automatically from the username you ent msgstr "Your hosting provider is detected automatically from the username you enter." #: src/Navigation.tsx:475 -#: src/screens/Search/modules/ExploreInterestsCard.tsx:68 +#: src/screens/Search/modules/ExploreInterestsCard.tsx:62 #: src/screens/Settings/ContentAndMediaSettings.tsx:94 #: src/screens/Settings/ContentAndMediaSettings.tsx:97 #: src/screens/Settings/InterestsSettings.tsx:49 @@ -14931,7 +14969,7 @@ msgctxt "toast" msgid "Your interests have been updated!" msgstr "" -#: src/screens/Search/modules/ExploreInterestsCard.tsx:95 +#: src/screens/Search/modules/ExploreInterestsCard.tsx:89 msgid "Your interests help us find what you like!" msgstr ""