diff --git a/.env.example b/.env.example index a4d21ac332..e18cda4e55 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,32 @@ -# Copy this to `.env` and `.env.test` files +# The env the app is running in e.g. development, testflight, production +EXPO_PUBLIC_ENV=development -BITDRIFT_API_KEY= -SENTRY_AUTH_TOKEN= -EXPO_PUBLIC_LOG_LEVEL=debug -EXPO_PUBLIC_LOG_DEBUG= +# This is the semver release version of the app, pulled from package.json +EXPO_PUBLIC_RELEASE_VERSION= + +# This is the commit hash that the current bundle was made from. EXPO_PUBLIC_BUNDLE_IDENTIFIER= + +# Should be formatted YYMMDDHH so that it increases for each build. EXPO_PUBLIC_BUNDLE_DATE=0 + +# The log level for the app's logger transports +EXPO_PUBLIC_LOG_LEVEL=debug + +# Enable debug logs for specific logger instances +EXPO_PUBLIC_LOG_DEBUG=session + +# Chat service DID +EXPO_PUBLIC_CHAT_PROXY_DID= + +# +# +# Bluesky specific values +# +# + +# Sentry DSN for telemetry +EXPO_PUBLIC_SENTRY_DSN= + +# Bitdrift API key. If undefined, Bitdrift will be disabled. +EXPO_PUBLIC_BITDRIFT_API_KEY= diff --git a/.github/workflows/build-and-push-bskyweb-aws.yaml b/.github/workflows/build-and-push-bskyweb-aws.yaml index 399e8669f0..6d573b0f71 100644 --- a/.github/workflows/build-and-push-bskyweb-aws.yaml +++ b/.github/workflows/build-and-push-bskyweb-aws.yaml @@ -43,12 +43,11 @@ jobs: tags: | type=sha,enable=true,priority=100,prefix=,suffix=,format=long - - name: Set outputs - id: vars + - name: Env + id: env run: | - echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - name: Build and push Docker image id: build-and-push @@ -62,8 +61,8 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max build-args: | - EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.vars.outputs.sha_short }} - SENTRY_DIST=${{ steps.vars.outputs.SENTRY_DIST }} - SENTRY_RELEASE=${{ steps.vars.outputs.SENTRY_RELEASE }} + EXPO_PUBLIC_ENV=production + EXPO_PUBLIC_RELEASE_VERSION=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} + EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} + EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_DSN=${{ secrets.SENTRY_DSN }} diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index e862a701fd..f1e75f87e5 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -62,23 +62,30 @@ jobs: - name: Check for i18n compilation errors run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi - - name: ✏️ Write environment variables + # 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_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .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 "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env echo "$json" > google-services.json - - name: Setup Sentry vars for build-time injection - id: sentry - run: | - echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT - - name: 🏗️ EAS Build - run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive + 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 }} + yarn use-build-number-with-bump + eas build -p android + --profile ${{ inputs.profile || 'testflight-android' }} + --local --output build.aab --non-interactive - name: ✍️ Rename Testflight bundle if: ${{ inputs.profile != 'production' }} @@ -140,7 +147,14 @@ jobs: - name: 🏗️ Build Production APK if: ${{ inputs.profile == 'production' }} - run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p android --profile production-apk --local --output build.apk --non-interactive + 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 }} + yarn use-build-number-with-bump + eas build -p android + --profile production-apk + --local --output build.apk --non-interactive - name: 🚀 Upload Production APK Artifact id: upload-artifact-production-apk diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml index a197695db7..f3759d0dd4 100644 --- a/.github/workflows/build-submit-ios.yml +++ b/.github/workflows/build-submit-ios.yml @@ -75,22 +75,29 @@ jobs: - 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 + # EXPO_PUBLIC_ENV is handled in eas.json - name: ✏️ Write environment variables + id: env run: | echo "${{ secrets.ENV_TOKEN }}" > .env - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .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 "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json - - name: Setup Sentry vars for build-time injection - id: sentry - run: | - echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT - - name: 🏗️ EAS Build - run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive + 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 }} + yarn use-build-number-with-bump + eas build -p ios + --profile ${{ inputs.profile || 'testflight' }} + --local --output build.ipa --non-interactive - name: 🚀 Deploy run: eas submit -p ios --non-interactive --path build.ipa diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml index 475bc087f1..1bd0b71c47 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -101,25 +101,30 @@ jobs: if: ${{ !steps.fingerprint.outputs.includes-changes }} uses: dcarbone/install-jq-action@v2 - - name: ✏️ Write environment variables + # eas.json not used here, set EXPO_PUBLIC_ENV + - name: Env + id: env if: ${{ !steps.fingerprint.outputs.includes-changes }} run: | export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' echo "${{ secrets.ENV_TOKEN }}" > .env - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env + echo "EXPO_PUBLIC_ENV=${{ inputs.channel || 'testflight' }}" >> .env + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env - echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env echo "$json" > google-services.json - - name: Setup Sentry vars for build-time injection - id: sentry - run: | - echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT - - name: 🏗️ Create Bundle if: ${{ !steps.fingerprint.outputs.includes-changes }} - run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export + 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 }} + yarn export - name: 📦 Package Bundle and 🚀 Deploy if: ${{ !steps.fingerprint.outputs.includes-changes }} @@ -205,16 +210,29 @@ jobs: - name: 🔤 Compile translations run: yarn intl:build - - name: ✏️ Write environment variables + # EXPO_PUBLIC_ENV is handled in eas.json + - name: Env + id: env run: | echo "${{ secrets.ENV_TOKEN }}" > .env - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .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 "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json - name: 🏗️ EAS Build - run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive + 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 }} + yarn use-build-number-with-bump + eas build -p ios + --profile testflight + --local --output build.ipa --non-interactive - name: 🚀 Deploy run: eas submit -p ios --non-interactive --path build.ipa @@ -282,17 +300,30 @@ jobs: - name: 🔤 Compile translations run: yarn intl:build - - name: ✏️ Write environment variables + # 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_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .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 "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env echo "$json" > google-services.json - name: 🏗️ EAS Build - run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive + 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 }} + yarn use-build-number-with-bump + eas build -p android + --profile testflight-android + --local --output build.apk --non-interactive - name: ⏰ Get a timestamp id: timestamp diff --git a/.github/workflows/pull-request-comment.yml b/.github/workflows/pull-request-comment.yml index e40eff6c7d..d043010027 100644 --- a/.github/workflows/pull-request-comment.yml +++ b/.github/workflows/pull-request-comment.yml @@ -152,23 +152,27 @@ jobs: - name: 🪛 Setup jq uses: dcarbone/install-jq-action@v2 - - name: ✏️ Write environment variables + - name: Env + id: env run: | export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' echo "${{ secrets.ENV_TOKEN }}" > .env - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env + echo "EXPO_PUBLIC_ENV=testflight" >> .env + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env - echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env echo "$json" > google-services.json - - name: Setup Sentry vars for build-time injection - id: sentry - run: | - echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT - - name: 🏗️ Create Bundle - run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="testflight" yarn export + 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 }} + yarn export - name: 📦 Package Bundle and 🚀 Deploy run: yarn use-build-number bash scripts/bundleUpdate.sh diff --git a/Dockerfile b/Dockerfile index 353d1e53bf..93b17a86b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24-bullseye AS build-env +FROM golang:1.24.5-bullseye AS build-env WORKDIR /usr/src/social-app @@ -19,28 +19,28 @@ ENV GOARCH="amd64" ENV CGO_ENABLED=1 ENV GOEXPERIMENT="loopvar" +# The latest git hash of the preview branch on render.com +# https://render.com/docs/docker-secrets#environment-variables-in-docker-builds +ARG RENDER_GIT_COMMIT + # # Expo # +ARG EXPO_PUBLIC_ENV +ENV EXPO_PUBLIC_ENV=${EXPO_PUBLIC_ENV:-development} +ARG EXPO_PUBLIC_RELEASE_VERSION +ENV EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER -ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev} - -# The latest git hash of the preview branch on render.com -ARG RENDER_GIT_COMMIT +# If not set by GitHub workflows, we're probably in Render +ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-$RENDER_GIT_COMMIT} # # Sentry # ARG SENTRY_AUTH_TOKEN ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown} -# Will fall back to package.json#version, but this is handled elsewhere -ARG SENTRY_RELEASE -ENV SENTRY_RELEASE=$SENTRY_RELEASE -ARG SENTRY_DIST -# Default to RENDER_GIT_COMMIT if not set by GitHub workflows -ENV SENTRY_DIST=${SENTRY_DIST:-$RENDER_GIT_COMMIT} -ARG SENTRY_DSN -ENV SENTRY_DSN=$SENTRY_DSN +ARG EXPO_PUBLIC_SENTRY_DSN +ENV EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN # # Copy everything into the container @@ -60,13 +60,16 @@ RUN \. "$NVM_DIR/nvm.sh" && \ nvm install $NODE_VERSION && \ nvm use $NODE_VERSION && \ echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \ + echo "EXPO_PUBLIC_ENV=$EXPO_PUBLIC_ENV" >> .env && \ + echo "EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION" >> .env && \ echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \ echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env && \ + echo "EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN" >> .env && \ npm install --global yarn && \ yarn && \ yarn intl:build 2>&1 | tee i18n.log && \ if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compile errors!\n\n"; fi && \ - EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER EXPO_PUBLIC_BUNDLE_DATE=$() SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$SENTRY_RELEASE SENTRY_DIST=$SENTRY_DIST SENTRY_DSN=$SENTRY_DSN yarn build-web + SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$EXPO_PUBLIC_RELEASE_VERSION SENTRY_DIST=$EXPO_PUBLIC_BUNDLE_IDENTIFIER yarn build-web # DEBUG RUN find ./bskyweb/static && find ./web-build/static diff --git a/Dockerfile.embedr b/Dockerfile.embedr index 4a536f70fc..7fa8b9ae53 100644 --- a/Dockerfile.embedr +++ b/Dockerfile.embedr @@ -1,4 +1,4 @@ -FROM golang:1.24-bullseye AS build-env +FROM golang:1.24.5-bullseye AS build-env WORKDIR /usr/src/social-app diff --git a/bskyweb/go.mod b/bskyweb/go.mod index 0a76e56eef..407ba7d55d 100644 --- a/bskyweb/go.mod +++ b/bskyweb/go.mod @@ -1,11 +1,9 @@ module github.com/bluesky-social/social-app/bskyweb -go 1.23.0 - -toolchain go1.24.5 +go 1.24.5 require ( - github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc + github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a github.com/flosch/pongo2/v6 v6.0.0 github.com/ipfs/go-log v1.0.5 github.com/joho/godotenv v1.5.1 @@ -16,6 +14,8 @@ require ( github.com/urfave/cli/v2 v2.25.7 ) +require github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect @@ -70,7 +70,6 @@ require ( github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/orandin/slog-gorm v1.3.2 // indirect github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect diff --git a/bskyweb/go.sum b/bskyweb/go.sum index 76a7c4b5bf..ffad3e8825 100644 --- a/bskyweb/go.sum +++ b/bskyweb/go.sum @@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc h1:chbGD59Cn1mw07kbq9Uvb8WUFIr1dcoL5TOhT+I9bV4= -github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng= +github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a h1:S12KN45uIkRglMHC8PqD/Vsz0+u3KbIaBF/6rit8/Pg= +github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= diff --git a/docs/build.md b/docs/build.md index 7817dd0957..87b653bdb9 100644 --- a/docs/build.md +++ b/docs/build.md @@ -89,9 +89,9 @@ If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running ### Adding bitdrift -Adding bitdirft is NOT required. You can keep `BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup. +Adding bitdrift is NOT required. You can keep `EXPO_PUBLIC_BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup. -However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `BITDRIFT_API_KEY` in your `.env` to enable bitdrift. +However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `EXPO_PUBLIC_BITDRIFT_API_KEY` in your `.env` to enable bitdrift. ### Adding and Updating Locales diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index a92e7be7f2..2a3a00ba7d 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -25,18 +25,17 @@ import { type ViewStyleProp, web, } from '#/alf' -import {Button} from '#/components/Button' +import {Button, ButtonText} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' -import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' import {InlineLinkText} from '#/components/Link' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' import {ProgressGuideList} from './ProgressGuide/List' -const MOBILE_CARD_WIDTH = 300 +const MOBILE_CARD_WIDTH = 165 function CardOuter({ children, @@ -48,8 +47,8 @@ function CardOuter({ - - - - - + return ( + + + + + + + + + + + + ) } @@ -243,10 +258,9 @@ export function ProfileGrid({ const t = useTheme() const {_} = useLingui() const moderationOpts = useModerationOpts() - const navigation = useNavigation() const {gtMobile} = useBreakpoints() const isLoading = isSuggestionsLoading || !moderationOpts - const maxLength = gtMobile ? 4 : 6 + const maxLength = gtMobile ? 3 : 6 const content = isLoading ? ( Array(maxLength) @@ -254,7 +268,14 @@ export function ProfileGrid({ .map((_, i) => ( + style={[ + gtMobile && + web([ + a.flex_0, + a.flex_grow, + {width: `calc(30% - ${a.gap_md.gap / 2}px)`}, + ]), + ]}> )) @@ -276,44 +297,69 @@ export function ProfileGrid({ }} style={[ a.flex_1, - gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}]), + gtMobile && + web([ + a.flex_0, + a.flex_grow, + {width: `calc(30% - ${a.gap_md.gap / 2}px)`}, + ]), ]}> {({hovered, pressed}) => ( - + - - { - logEvent('suggestedUser:follow', { - logContext: - viewContext === 'feed' - ? 'InterstitialDiscover' - : 'InterstitialProfile', - location: 'Card', - recId, - position: index, - }) - }} - /> - - + + + + + + + { + logEvent('suggestedUser:follow', { + logContext: + viewContext === 'feed' + ? 'InterstitialDiscover' + : 'InterstitialProfile', + location: 'Card', + recId, + position: index, + }) + }} + /> )} @@ -333,36 +379,30 @@ export function ProfileGrid({ - + {viewContext === 'profile' ? ( Similar accounts ) : ( Suggested for you )} - + + See more + {gtMobile ? ( - - + + {content} - - - - Browse more suggestions - - - ) : ( @@ -371,29 +411,12 @@ export function ProfileGrid({ horizontal showsHorizontalScrollIndicator={false} snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap} - decelerationRate="fast"> - + decelerationRate="fast" + style={[a.overflow_visible]}> + {content} - + @@ -403,6 +426,32 @@ export function ProfileGrid({ ) } +function SeeMoreSuggestedProfilesCard() { + const navigation = useNavigation() + const t = useTheme() + const {_} = useLingui() + + return ( + + ) +} + export function SuggestedFeeds() { const numFeedsToDisplay = 3 const t = useTheme() diff --git a/src/components/PostControls/DiscoverDebug.tsx b/src/components/PostControls/DiscoverDebug.tsx index 796981f0c2..403b50ccab 100644 --- a/src/components/PostControls/DiscoverDebug.tsx +++ b/src/components/PostControls/DiscoverDebug.tsx @@ -2,13 +2,13 @@ import {Pressable} from 'react-native' import * as Clipboard from 'expo-clipboard' import {t} from '@lingui/macro' -import {IS_INTERNAL} from '#/lib/app-info' import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' import {useGate} from '#/lib/statsig/statsig' import {useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Text} from '#/components/Typography' +import {IS_INTERNAL} from '#/env' export function DiscoverDebug({ feedContext, diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index f0ef9ed055..ecc3d01741 100644 --- a/src/components/PostControls/PostMenu/PostMenuItems.tsx +++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx @@ -17,7 +17,6 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {IS_INTERNAL} from '#/lib/app-info' import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' import {useOpenLink} from '#/lib/hooks/useOpenLink' import {getCurrentRoute} from '#/lib/routes/helpers' @@ -83,6 +82,7 @@ import { useReportDialogControl, } from '#/components/moderation/ReportDialog' import * as Prompt from '#/components/Prompt' +import {IS_INTERNAL} from '#/env' import * as bsky from '#/types/bsky' let PostMenuItems = ({ diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index e01c27655f..f12d922fd3 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -20,7 +20,13 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {useSession} from '#/state/session' import * as Toast from '#/view/com/util/Toast' import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, platform, useTheme} from '#/alf' +import { + atoms as a, + platform, + type TextStyleProp, + useTheme, + type ViewStyleProp, +} from '#/alf' import { Button, ButtonIcon, @@ -136,12 +142,14 @@ export function Avatar({ onPress, disabledPreview, liveOverride, + size = 40, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts onPress?: () => void disabledPreview?: boolean liveOverride?: boolean + size?: number }) { const moderation = moderateProfile(profile, moderationOpts) @@ -149,7 +157,7 @@ export function Avatar({ return disabledPreview ? ( ) : ( @@ -274,7 +282,7 @@ export function Name({ ) const verification = useSimpleVerificationState({profile}) return ( - + + ) +} + export function Description({ profile: profileUnshadowed, numberOfLines = 3, + style, }: { profile: bsky.profile.AnyProfileView numberOfLines?: number -}) { +} & TextStyleProp) { const profile = useProfileShadow(profileUnshadowed) const rt = useMemo(() => { if (!('description' in profile)) return @@ -369,7 +396,7 @@ export function Description({ diff --git a/src/env.ts b/src/env.ts deleted file mode 100644 index 32ce70670b..0000000000 --- a/src/env.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || '' -export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as - | 'debug' - | 'info' - | 'warn' - | 'error' diff --git a/src/env/common.ts b/src/env/common.ts new file mode 100644 index 0000000000..e68e9fab80 --- /dev/null +++ b/src/env/common.ts @@ -0,0 +1,79 @@ +import {type Did} from '@atproto/api' + +import packageJson from '#/../package.json' + +/** + * The semver version of the app, as defined in `package.json.` + * + * N.B. The fallback is needed for Render.com deployments + */ +export const RELEASE_VERSION: string = + process.env.EXPO_PUBLIC_RELEASE_VERSION || packageJson.version + +/** + * The env the app is running in e.g. development, testflight, production + */ +export const ENV: string = process.env.EXPO_PUBLIC_ENV + +/** + * Indicates whether the app is running in TestFlight + */ +export const IS_TESTFLIGHT = ENV === 'testflight' + +/** + * Indicates whether the app is __DEV__ + */ +export const IS_DEV = __DEV__ + +/** + * Indicates whether the app is __DEV__ or TestFlight + */ +export const IS_INTERNAL = IS_DEV || IS_TESTFLIGHT + +/** + * The commit hash that the current bundle was made from. The user can + * see the commit hash in the app's settings along with the other version info. + * Useful for debugging/reporting. + */ +export const BUNDLE_IDENTIFIER: string = + process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER || 'dev' + +/** + * This will always be in the format of YYMMDDHH, so that it always increases + * for each build. This should only be used for StatSig reporting and shouldn't + * be used to identify a specific bundle. + */ +export const BUNDLE_DATE: number = !process.env.EXPO_PUBLIC_BUNDLE_DATE + ? 0 + : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE) + +/** + * The log level for the app. + */ +export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as + | 'debug' + | 'info' + | 'warn' + | 'error' + +/** + * Enable debug logs for specific logger instances + */ +export const LOG_DEBUG: string = process.env.EXPO_PUBLIC_LOG_DEBUG || '' + +/** + * The DID of the chat service to proxy to + */ +export const CHAT_PROXY_DID: Did = + process.env.EXPO_PUBLIC_CHAT_PROXY_DID || 'did:web:api.bsky.chat' + +/** + * Sentry DSN for telemetry + */ +export const SENTRY_DSN: string | undefined = process.env.EXPO_PUBLIC_SENTRY_DSN + +/** + * Bitdrift API key. If undefined, Bitdrift should be disabled. + */ +export const BITDRIFT_API_KEY: string | undefined = + process.env.EXPO_PUBLIC_BITDRIFT_API_KEY diff --git a/src/env/index.ts b/src/env/index.ts new file mode 100644 index 0000000000..8558c55b5d --- /dev/null +++ b/src/env/index.ts @@ -0,0 +1,19 @@ +import {nativeBuildVersion} from 'expo-application' + +import {BUNDLE_IDENTIFIER, IS_TESTFLIGHT, RELEASE_VERSION} from '#/env/common' + +export * from '#/env/common' + +/** + * The semver version of the app, specified in our `package.json`.file. On + * iOs/Android, the native build version is appended to the semver version, so + * that it can be used to identify a specific build. + */ +export const APP_VERSION = `${RELEASE_VERSION}.${nativeBuildVersion}` + +/** + * The short commit hash and environment of the current bundle. + */ +export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${ + __DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod' +})` diff --git a/src/env/index.web.ts b/src/env/index.web.ts new file mode 100644 index 0000000000..66087749b7 --- /dev/null +++ b/src/env/index.web.ts @@ -0,0 +1,15 @@ +import {BUNDLE_IDENTIFIER, RELEASE_VERSION} from '#/env/common' + +export * from '#/env/common' + +/** + * The semver version of the app, specified in our `package.json`.file. On + * iOs/Android, the native build version is appended to the semver version, so + * that it can be used to identify a specific build. + */ +export const APP_VERSION = RELEASE_VERSION + +/** + * The short commit hash and environment of the current bundle. + */ +export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${__DEV__ ? 'dev' : 'prod'})` diff --git a/src/lib/app-info.ts b/src/lib/app-info.ts deleted file mode 100644 index 0749087eae..0000000000 --- a/src/lib/app-info.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application' - -export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight' -export const IS_INTERNAL = __DEV__ || IS_TESTFLIGHT - -// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings -// along with the other version info. Useful for debugging/reporting. -export const BUNDLE_IDENTIFIER = process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? '' - -// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used -// for Statsig reporting and shouldn't be used to identify a specific bundle. -export const BUNDLE_DATE = - IS_TESTFLIGHT || __DEV__ ? 0 : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE) - -export const appVersion = `${nativeApplicationVersion}.${nativeBuildVersion}` -export const bundleInfo = `${BUNDLE_IDENTIFIER} (${ - __DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod' -})` diff --git a/src/lib/app-info.web.ts b/src/lib/app-info.web.ts deleted file mode 100644 index 1530d99769..0000000000 --- a/src/lib/app-info.web.ts +++ /dev/null @@ -1,18 +0,0 @@ -import packageDotJson from '../../package.json' - -export const IS_TESTFLIGHT = false -export const IS_INTERNAL = __DEV__ - -// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings -// along with the other version info. Useful for debugging/reporting. -export const BUNDLE_IDENTIFIER = - process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? 'dev' - -// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used -// for Statsig reporting and shouldn't be used to identify a specific bundle. -export const BUNDLE_DATE = __DEV__ - ? 0 - : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE) - -export const appVersion = packageDotJson.version -export const bundleInfo = `${BUNDLE_IDENTIFIER} (${__DEV__ ? 'dev' : 'prod'})` diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts index 864d5d6970..ba46b60552 100644 --- a/src/lib/hooks/useOTAUpdates.ts +++ b/src/lib/hooks/useOTAUpdates.ts @@ -10,9 +10,9 @@ import { useUpdates, } from 'expo-updates' -import {IS_TESTFLIGHT} from '#/lib/app-info' import {logger} from '#/logger' import {isIOS} from '#/platform/detection' +import {IS_TESTFLIGHT} from '#/env' const MINIMUM_MINIMIZE_TIME = 15 * 60e3 diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index f2d3ffca9b..1091c82e04 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -3,12 +3,11 @@ import {Platform} from 'react-native' import {AppState, type AppStateStatus} from 'react-native' import {Statsig, StatsigProvider} from 'statsig-react-native-expo' -import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info' import {logger} from '#/logger' import {type MetricEvents} from '#/logger/metrics' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' -import packageDotJson from '../../../package.json' +import * as env from '#/env' import {useSession} from '../../state/session' import {timeout} from '../async/timeout' import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback' @@ -49,12 +48,11 @@ export type {MetricEvents as LogEvents} function createStatsigOptions(prefetchUsers: StatsigUser[]) { return { environment: { - tier: - process.env.NODE_ENV === 'development' - ? 'development' - : IS_TESTFLIGHT - ? 'staging' - : 'production', + tier: env.IS_DEV + ? 'development' + : env.IS_TESTFLIGHT + ? 'staging' + : 'production', }, // Don't block on waiting for network. The fetched config will kick in on next load. // This ensures the UI is always consistent and doesn't update mid-session. @@ -212,9 +210,9 @@ function toStatsigUser(did: string | undefined): StatsigUser { refSrc, refUrl, platform: Platform.OS as 'ios' | 'android' | 'web', - appVersion: packageDotJson.version, - bundleIdentifier: BUNDLE_IDENTIFIER, - bundleDate: BUNDLE_DATE, + appVersion: env.RELEASE_VERSION, + bundleIdentifier: env.BUNDLE_IDENTIFIER, + bundleDate: env.BUNDLE_DATE, appLanguage: languagePrefs.appLanguage, contentLanguages: languagePrefs.contentLanguages, }, diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 69532591c3..3a5480a5cd 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -966,8 +966,8 @@ msgstr "" #: src/components/hooks/useFollowMethods.ts:35 #: src/components/hooks/useFollowMethods.ts:50 -#: src/components/ProfileCard.tsx:457 -#: src/components/ProfileCard.tsx:478 +#: src/components/ProfileCard.tsx:484 +#: src/components/ProfileCard.tsx:505 #: src/view/com/profile/FollowButton.tsx:38 #: src/view/com/profile/FollowButton.tsx:48 msgid "An issue occurred, please try again." @@ -1262,7 +1262,7 @@ msgid "Birthday" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:757 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:319 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:320 #: src/view/com/profile/ProfileMenu.tsx:473 msgid "Block" msgstr "" @@ -1423,23 +1423,20 @@ msgstr "" msgid "Books" msgstr "" -#: src/components/FeedInterstitials.tsx:379 +#: src/components/FeedInterstitials.tsx:436 msgid "Browse more accounts on the Explore page" msgstr "" -#: src/components/FeedInterstitials.tsx:517 +#: src/components/FeedInterstitials.tsx:566 msgid "Browse more feeds on the Explore page" msgstr "" -#: src/components/FeedInterstitials.tsx:359 -#: src/components/FeedInterstitials.tsx:362 -#: src/components/FeedInterstitials.tsx:498 -#: src/components/FeedInterstitials.tsx:501 +#: src/components/FeedInterstitials.tsx:547 +#: src/components/FeedInterstitials.tsx:550 msgid "Browse more suggestions" msgstr "" -#: src/components/FeedInterstitials.tsx:387 -#: src/components/FeedInterstitials.tsx:526 +#: src/components/FeedInterstitials.tsx:575 msgid "Browse more suggestions on the Explore page" msgstr "" @@ -3618,7 +3615,7 @@ msgid "Flexible" msgstr "" #. User is not following this account, click to follow -#: src/components/ProfileCard.tsx:490 +#: src/components/ProfileCard.tsx:517 #: src/components/ProfileHoverCard/index.web.tsx:494 #: src/components/ProfileHoverCard/index.web.tsx:505 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:245 @@ -3696,7 +3693,7 @@ msgid "Followers you know" msgstr "" #. User is following this account, click to unfollow -#: src/components/ProfileCard.tsx:484 +#: src/components/ProfileCard.tsx:511 #: src/components/ProfileHoverCard/index.web.tsx:493 #: src/components/ProfileHoverCard/index.web.tsx:504 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:241 @@ -3711,7 +3708,7 @@ msgctxt "feed-name" msgid "Following" msgstr "" -#: src/components/ProfileCard.tsx:447 +#: src/components/ProfileCard.tsx:474 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:89 msgid "Following {0}" msgstr "" @@ -5459,7 +5456,7 @@ msgstr "" msgid "No likes yet" msgstr "" -#: src/components/ProfileCard.tsx:469 +#: src/components/ProfileCard.tsx:496 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:110 msgid "No longer following {0}" msgstr "" @@ -7398,6 +7395,18 @@ msgstr "" msgid "See jobs at Bluesky" msgstr "" +#: src/components/FeedInterstitials.tsx:397 +msgid "See more" +msgstr "" + +#: src/components/FeedInterstitials.tsx:444 +msgid "See more accounts you might like" +msgstr "" + +#: src/components/FeedInterstitials.tsx:395 +msgid "See more suggested profiles on the Explore page" +msgstr "" + #: src/view/screens/SavedFeeds.tsx:213 msgid "See this guide" msgstr "" @@ -7975,7 +7984,7 @@ msgstr "" msgid "Signed in as @{0}" msgstr "" -#: src/components/FeedInterstitials.tsx:343 +#: src/components/FeedInterstitials.tsx:389 msgid "Similar accounts" msgstr "" @@ -8005,7 +8014,7 @@ msgstr "" msgid "Some of your verifications are invalid." msgstr "" -#: src/components/FeedInterstitials.tsx:480 +#: src/components/FeedInterstitials.tsx:529 msgid "Some other feeds you might like" msgstr "" @@ -8237,7 +8246,7 @@ msgstr "" msgid "Suggested Accounts" msgstr "" -#: src/components/FeedInterstitials.tsx:345 +#: src/components/FeedInterstitials.tsx:391 msgid "Suggested for you" msgstr "" @@ -8421,7 +8430,7 @@ msgstr "" msgid "That's everything!" msgstr "" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:315 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:316 #: src/view/com/profile/ProfileMenu.tsx:461 msgid "The account will be able to interact with you after unblocking." msgstr "" @@ -9046,7 +9055,7 @@ msgstr "" #: src/components/dms/MessagesListBlockedFooter.tsx:112 #: src/components/dms/MessagesListBlockedFooter.tsx:119 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:203 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:319 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:320 #: src/view/com/profile/ProfileMenu.tsx:473 #: src/view/screens/ProfileList.tsx:723 msgid "Unblock" @@ -9064,7 +9073,7 @@ msgstr "" msgid "Unblock account" msgstr "" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:313 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:314 #: src/view/com/profile/ProfileMenu.tsx:455 msgid "Unblock Account?" msgstr "" @@ -9571,7 +9580,7 @@ msgstr "" msgid "View {0}'s avatar" msgstr "" -#: src/components/ProfileCard.tsx:118 +#: src/components/ProfileCard.tsx:124 #: src/screens/Profile/components/ProfileFeedHeader.tsx:454 #: src/screens/Search/components/SearchProfileCard.tsx:36 #: src/screens/VideoFeed/index.tsx:790 diff --git a/src/logger/bitdrift/setup/index.ts b/src/logger/bitdrift/setup/index.ts index d6af3fe24b..dd2560acc6 100644 --- a/src/logger/bitdrift/setup/index.ts +++ b/src/logger/bitdrift/setup/index.ts @@ -2,8 +2,7 @@ import {init, SessionStrategy} from '@bitdrift/react-native' import {Statsig} from 'statsig-react-native-expo' import {initPromise} from '#/lib/statsig/statsig' - -const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY +import {BITDRIFT_API_KEY} from '#/env' initPromise.then(() => { let isEnabled = false diff --git a/src/logger/index.ts b/src/logger/index.ts index e7aaf666a0..998d025818 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -14,9 +14,10 @@ import { } from '#/logger/types' import {enabledLogLevels} from '#/logger/util' import {isNative} from '#/platform/detection' +import {ENV} from '#/env' const TRANSPORTS: Transport[] = (function configureTransports() { - switch (process.env.NODE_ENV) { + switch (ENV) { case 'production': { return [sentryTransport, isNative && bitdriftTransport].filter( Boolean, diff --git a/src/logger/sentry/setup/index.ts b/src/logger/sentry/setup/index.ts index f05a7fc833..d062f05d2b 100644 --- a/src/logger/sentry/setup/index.ts +++ b/src/logger/sentry/setup/index.ts @@ -1,32 +1,15 @@ -/** - * Importing these separately from `platform/detection` and `lib/app-info` to - * avoid future conflicts and/or circular deps - */ - import {init} from '@sentry/react-native' -import pkgJson from '#/../package.json' - -/** - * Examples: - * - `dev` - * - `1.99.0` - */ -const release = process.env.SENTRY_RELEASE || pkgJson.version - -/** - * The latest deployed commit hash - */ -const dist = process.env.SENTRY_DIST || 'dev' +import * as env from '#/env' init({ - enabled: !__DEV__ && !!process.env.SENTRY_DSN, + enabled: !env.IS_DEV && !!env.SENTRY_DSN, autoSessionTracking: false, - dsn: process.env.SENTRY_DSN, + dsn: env.SENTRY_DSN, debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production - environment: process.env.NODE_ENV, - dist, - release, + environment: env.ENV, + dist: env.BUNDLE_IDENTIFIER, + release: env.RELEASE_VERSION, ignoreErrors: [ /* * Unknown internals errors diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx index 0ce127ff3f..e48841d9f0 100644 --- a/src/screens/Settings/AboutSettings.tsx +++ b/src/screens/Settings/AboutSettings.tsx @@ -9,7 +9,6 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {useMutation} from '@tanstack/react-query' import {Statsig} from 'statsig-react-native-expo' -import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info' import {STATUS_PAGE_URL} from '#/lib/constants' import {type CommonNavigatorParams} from '#/lib/routes/types' import {isAndroid, isIOS, isNative} from '#/platform/detection' @@ -23,6 +22,7 @@ import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/i import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' +import * as env from '#/env' import {useDemoMode} from '#/storage/hooks/demo-mode' import {useDevMode} from '#/storage/hooks/dev-mode' import {OTAInfo} from './components/OTAInfo' @@ -123,7 +123,7 @@ export function AboutSettingsScreen({}: Props) { )} { const newDevModeEnabled = !devModeEnabled @@ -146,15 +146,15 @@ export function AboutSettingsScreen({}: Props) { }} onPress={() => { setStringAsync( - `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`, + `Build version: ${env.APP_VERSION}; Bundle info: ${env.APP_METADATA}; Bundle date: ${env.BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`, ) Toast.show(_(msg`Copied build version to clipboard`)) }}> - Version {appVersion} + Version {env.APP_VERSION} - {bundleInfo} + {env.APP_METADATA} {devModeEnabled && ( <> diff --git a/src/screens/Settings/AppIconSettings/index.tsx b/src/screens/Settings/AppIconSettings/index.tsx index 954bac68a5..799873c2d9 100644 --- a/src/screens/Settings/AppIconSettings/index.tsx +++ b/src/screens/Settings/AppIconSettings/index.tsx @@ -3,20 +3,20 @@ import {Alert, View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon' -import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' -import {IS_INTERNAL} from '#/lib/app-info' import {PressableScale} from '#/lib/custom-animations/PressableScale' -import {CommonNavigatorParams} from '#/lib/routes/types' +import {type CommonNavigatorParams} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' import {isAndroid} from '#/platform/detection' import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage' -import {AppIconSet} from '#/screens/Settings/AppIconSettings/types' +import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types' import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets' import {atoms as a, useTheme} from '#/alf' import * as Toggle from '#/components/forms/Toggle' import * as Layout from '#/components/Layout' import {Text} from '#/components/Typography' +import {IS_INTERNAL} from '#/env' type Props = NativeStackScreenProps export function AppIconSettingsScreen({}: Props) { diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx index d0158aaa84..492d6d172b 100644 --- a/src/screens/Settings/AppearanceSettings.tsx +++ b/src/screens/Settings/AppearanceSettings.tsx @@ -8,7 +8,6 @@ import Animated, { import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {IS_INTERNAL} from '#/lib/app-info' import { type CommonNavigatorParams, type NativeStackScreenProps, @@ -26,6 +25,7 @@ import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/T import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' import * as Layout from '#/components/Layout' import {Text} from '#/components/Typography' +import {IS_INTERNAL} from '#/env' import * as SettingsList from './components/SettingsList' type Props = NativeStackScreenProps diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index b712c054cb..719bbf9a29 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -9,7 +9,6 @@ import {useNavigation} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {useActorStatus} from '#/lib/actor-status' -import {IS_INTERNAL} from '#/lib/app-info' import {HELP_DESK_URL} from '#/lib/constants' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' @@ -66,6 +65,7 @@ import { shouldShowVerificationCheckButton, VerificationCheckButton, } from '#/components/verification/VerificationCheckButton' +import {IS_INTERNAL} from '#/env' import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' type Props = NativeStackScreenProps diff --git a/src/state/queries/messages/const.ts b/src/state/queries/messages/const.ts index 7642c5d7b2..1c5519a633 100644 --- a/src/state/queries/messages/const.ts +++ b/src/state/queries/messages/const.ts @@ -1,3 +1,5 @@ +import {CHAT_PROXY_DID} from '#/env' + export const DM_SERVICE_HEADERS = { - 'atproto-proxy': 'did:web:api.bsky.chat#bsky_chat', + 'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`, } diff --git a/src/state/session/logging.ts b/src/state/session/logging.ts index 98de5a3964..bf847f08f5 100644 --- a/src/state/session/logging.ts +++ b/src/state/session/logging.ts @@ -1,11 +1,11 @@ -import {AtpSessionData, AtpSessionEvent} from '@atproto/api' +import {type AtpSessionData, type AtpSessionEvent} from '@atproto/api' import {sha256} from 'js-sha256' import {Statsig} from 'statsig-react-native-expo' -import {IS_INTERNAL} from '#/lib/app-info' -import {Schema} from '../persisted' -import {Action, State} from './reducer' -import {SessionAccount} from './types' +import {IS_INTERNAL} from '#/env' +import {type Schema} from '../persisted' +import {type Action, type State} from './reducer' +import {type SessionAccount} from './types' type Reducer = (state: State, action: Action) => State diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 6a931d9a40..496b771821 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -101,13 +101,9 @@ export const Link = memo(function Link({ {name: 'activate', label: title}, ] - const dataSet = useMemo(() => { - const ds = {...dataSetProp} - if (anchorNoUnderline) { - ds.noUnderline = 1 - } - return ds - }, [dataSetProp, anchorNoUnderline]) + const dataSet = anchorNoUnderline + ? {...dataSetProp, noUnderline: 1} + : dataSetProp if (noFeedback) { return ( @@ -125,6 +121,8 @@ export const Link = memo(function Link({ onAccessibilityAction?.(e) } }} + // @ts-ignore web only -sfn + dataSet={dataSet} {...props} android_ripple={{ color: t.atoms.bg_contrast_25.backgroundColor, @@ -198,13 +196,9 @@ export const TextLink = memo(function TextLink({ console.error('Unable to detect mismatching label') } - const dataSet = useMemo(() => { - const ds = {...dataSetProp} - if (anchorNoUnderline) { - ds.noUnderline = 1 - } - return ds - }, [dataSetProp, anchorNoUnderline]) + const dataSet = anchorNoUnderline + ? {...dataSetProp, noUnderline: 1} + : dataSetProp const onPress = useCallback( (e?: Event) => { diff --git a/src/view/com/util/Toast.style.tsx b/src/view/com/util/Toast.style.tsx new file mode 100644 index 0000000000..3869e6890b --- /dev/null +++ b/src/view/com/util/Toast.style.tsx @@ -0,0 +1,201 @@ +import {select, type Theme} from '#/alf' +import {Check_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' +import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' +import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' + +export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' + +export type LegacyToastType = + | 'xmark' + | 'exclamation-circle' + | 'check' + | 'clipboard-check' + | 'circle-exclamation' + +export const convertLegacyToastType = ( + type: ToastType | LegacyToastType, +): ToastType => { + switch (type) { + // these ones are fine + case 'default': + case 'success': + case 'error': + case 'warning': + case 'info': + return type + // legacy ones need conversion + case 'xmark': + return 'error' + case 'exclamation-circle': + return 'warning' + case 'check': + return 'success' + case 'clipboard-check': + return 'success' + case 'circle-exclamation': + return 'warning' + default: + return 'default' + } +} + +export const TOAST_ANIMATION_CONFIG = { + duration: 300, + damping: 15, + stiffness: 150, + mass: 0.8, + overshootClamping: false, + restSpeedThreshold: 0.01, + restDisplacementThreshold: 0.01, +} + +export const TOAST_TYPE_TO_ICON = { + default: SuccessIcon, + success: SuccessIcon, + error: ErrorIcon, + warning: WarningIcon, + info: CircleInfo, +} + +export const getToastTypeStyles = (t: Theme) => ({ + default: { + backgroundColor: select(t.name, { + light: t.atoms.bg_contrast_25.backgroundColor, + dim: t.atoms.bg_contrast_100.backgroundColor, + dark: t.atoms.bg_contrast_100.backgroundColor, + }), + borderColor: select(t.name, { + light: t.atoms.border_contrast_low.borderColor, + dim: t.atoms.border_contrast_high.borderColor, + dark: t.atoms.border_contrast_high.borderColor, + }), + iconColor: select(t.name, { + light: t.atoms.text_contrast_medium.color, + dim: t.atoms.text_contrast_medium.color, + dark: t.atoms.text_contrast_medium.color, + }), + textColor: select(t.name, { + light: t.atoms.text_contrast_medium.color, + dim: t.atoms.text_contrast_medium.color, + dark: t.atoms.text_contrast_medium.color, + }), + }, + success: { + backgroundColor: select(t.name, { + light: t.palette.primary_100, + dim: t.palette.primary_100, + dark: t.palette.primary_50, + }), + borderColor: select(t.name, { + light: t.palette.primary_500, + dim: t.palette.primary_500, + dark: t.palette.primary_500, + }), + iconColor: select(t.name, { + light: t.palette.primary_500, + dim: t.palette.primary_600, + dark: t.palette.primary_600, + }), + textColor: select(t.name, { + light: t.palette.primary_500, + dim: t.palette.primary_600, + dark: t.palette.primary_600, + }), + }, + error: { + backgroundColor: select(t.name, { + light: t.palette.negative_200, + dim: t.palette.negative_25, + dark: t.palette.negative_25, + }), + borderColor: select(t.name, { + light: t.palette.negative_300, + dim: t.palette.negative_300, + dark: t.palette.negative_300, + }), + iconColor: select(t.name, { + light: t.palette.negative_600, + dim: t.palette.negative_600, + dark: t.palette.negative_600, + }), + textColor: select(t.name, { + light: t.palette.negative_600, + dim: t.palette.negative_600, + dark: t.palette.negative_600, + }), + }, + warning: { + backgroundColor: select(t.name, { + light: t.atoms.bg_contrast_25.backgroundColor, + dim: t.atoms.bg_contrast_100.backgroundColor, + dark: t.atoms.bg_contrast_100.backgroundColor, + }), + borderColor: select(t.name, { + light: t.atoms.border_contrast_low.borderColor, + dim: t.atoms.border_contrast_high.borderColor, + dark: t.atoms.border_contrast_high.borderColor, + }), + iconColor: select(t.name, { + light: t.atoms.text_contrast_medium.color, + dim: t.atoms.text_contrast_medium.color, + dark: t.atoms.text_contrast_medium.color, + }), + textColor: select(t.name, { + light: t.atoms.text_contrast_medium.color, + dim: t.atoms.text_contrast_medium.color, + dark: t.atoms.text_contrast_medium.color, + }), + }, + info: { + backgroundColor: select(t.name, { + light: t.atoms.bg_contrast_25.backgroundColor, + dim: t.atoms.bg_contrast_100.backgroundColor, + dark: t.atoms.bg_contrast_100.backgroundColor, + }), + borderColor: select(t.name, { + light: t.atoms.border_contrast_low.borderColor, + dim: t.atoms.border_contrast_high.borderColor, + dark: t.atoms.border_contrast_high.borderColor, + }), + iconColor: select(t.name, { + light: t.atoms.text_contrast_medium.color, + dim: t.atoms.text_contrast_medium.color, + dark: t.atoms.text_contrast_medium.color, + }), + textColor: select(t.name, { + light: t.atoms.text_contrast_medium.color, + dim: t.atoms.text_contrast_medium.color, + dark: t.atoms.text_contrast_medium.color, + }), + }, +}) + +export const getToastWebAnimationStyles = () => ({ + entering: { + animation: 'toastFadeIn 0.3s ease-out forwards', + }, + exiting: { + animation: 'toastFadeOut 0.2s ease-in forwards', + }, +}) + +export const TOAST_WEB_KEYFRAMES = ` + @keyframes toastFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes toastFadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } + } +` diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx index 56c6780ad3..54ef7042d2 100644 --- a/src/view/com/util/Toast.tsx +++ b/src/view/com/util/Toast.tsx @@ -6,8 +6,8 @@ import { GestureHandlerRootView, } from 'react-native-gesture-handler' import Animated, { - FadeInUp, - FadeOutUp, + FadeIn, + FadeOut, runOnJS, useAnimatedReaction, useAnimatedStyle, @@ -17,37 +17,55 @@ import Animated, { } from 'react-native-reanimated' import RootSiblings from 'react-native-root-siblings' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import { - FontAwesomeIcon, - type Props as FontAwesomeProps, -} from '@fortawesome/react-native-fontawesome' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import { + convertLegacyToastType, + getToastTypeStyles, + type LegacyToastType, + TOAST_ANIMATION_CONFIG, + TOAST_TYPE_TO_ICON, + type ToastType, +} from '#/view/com/util/Toast.style' import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' const TIMEOUT = 2e3 +// Use type overloading to mark certain types as deprecated -sfn +// https://stackoverflow.com/a/78325851/13325987 +export function show(message: string, type?: ToastType): void +/** + * @deprecated type is deprecated - use one of `'default' | 'success' | 'error' | 'warning' | 'info'` + */ +export function show(message: string, type?: LegacyToastType): void export function show( message: string, - icon: FontAwesomeProps['icon'] = 'check', -) { + type: ToastType | LegacyToastType = 'default', +): void { if (process.env.NODE_ENV === 'test') { return } + AccessibilityInfo.announceForAccessibility(message) const item = new RootSiblings( - item.destroy()} />, + ( + item.destroy()} + /> + ), ) } function Toast({ message, - icon, + type, destroy, }: { message: string - icon: FontAwesomeProps['icon'] + type: ToastType destroy: () => void }) { const t = useTheme() @@ -56,6 +74,10 @@ function Toast({ const dismissSwipeTranslateY = useSharedValue(0) const [cardHeight, setCardHeight] = useState(0) + const toastStyles = getToastTypeStyles(t) + const colors = toastStyles[type] + const IconComponent = TOAST_TYPE_TO_ICON[type] + // for the exit animation to work on iOS the animated component // must not be the root component // so we need to wrap it in a view and unmount the toast ahead of time @@ -159,55 +181,52 @@ function Toast({ pointerEvents="box-none"> {alive && ( - setCardHeight(evt.nativeEvent.layout.height)} - accessibilityRole="alert" - accessible={true} - accessibilityLabel={message} - accessibilityHint="" - onAccessibilityEscape={hideAndDestroyImmediately} - style={[ - a.flex_1, - t.name === 'dark' ? t.atoms.bg_contrast_25 : t.atoms.bg, - a.shadow_lg, - t.atoms.border_contrast_medium, - a.rounded_sm, - a.border, - animatedStyle, - ]}> - - - - - - - - {message} - - + entering={FadeIn.duration(TOAST_ANIMATION_CONFIG.duration)} + exiting={FadeOut.duration(TOAST_ANIMATION_CONFIG.duration * 0.7)} + onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)} + accessibilityRole="alert" + accessible={true} + accessibilityLabel={message} + accessibilityHint="" + onAccessibilityEscape={hideAndDestroyImmediately} + style={[ + a.flex_1, + {backgroundColor: colors.backgroundColor}, + a.shadow_sm, + {borderColor: colors.borderColor, borderWidth: 1}, + a.rounded_sm, + animatedStyle, + ]}> + + + + - - + + + {message} + + + + )} diff --git a/src/view/com/util/Toast.web.tsx b/src/view/com/util/Toast.web.tsx index d3b7bda33e..6b99b30bf3 100644 --- a/src/view/com/util/Toast.web.tsx +++ b/src/view/com/util/Toast.web.tsx @@ -4,17 +4,23 @@ import {useEffect, useState} from 'react' import {Pressable, StyleSheet, Text, View} from 'react-native' + import { - FontAwesomeIcon, - type FontAwesomeIconStyle, - type Props as FontAwesomeProps, -} from '@fortawesome/react-native-fontawesome' + convertLegacyToastType, + getToastTypeStyles, + getToastWebAnimationStyles, + type LegacyToastType, + TOAST_TYPE_TO_ICON, + TOAST_WEB_KEYFRAMES, + type ToastType, +} from '#/view/com/util/Toast.style' +import {atoms as a, useTheme} from '#/alf' const DURATION = 3500 interface ActiveToast { text: string - icon: FontAwesomeProps['icon'] + type: ToastType } type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void @@ -28,21 +34,82 @@ let toastTimeout: NodeJS.Timeout | undefined type ToastContainerProps = {} export const ToastContainer: React.FC = ({}) => { const [activeToast, setActiveToast] = useState() + const [isExiting, setIsExiting] = useState(false) + useEffect(() => { globalSetActiveToast = (t: ActiveToast | undefined) => { - setActiveToast(t) + if (!t && activeToast) { + setIsExiting(true) + setTimeout(() => { + setActiveToast(t) + setIsExiting(false) + }, 200) + } else { + setActiveToast(t) + setIsExiting(false) + } } - }) + }, [activeToast]) + + useEffect(() => { + const styleId = 'toast-animations' + if (!document.getElementById(styleId)) { + const style = document.createElement('style') + style.id = styleId + style.textContent = TOAST_WEB_KEYFRAMES + document.head.appendChild(style) + } + }, []) + + const t = useTheme() + + const toastTypeStyles = getToastTypeStyles(t) + const toastStyles = activeToast + ? toastTypeStyles[activeToast.type] + : toastTypeStyles.default + + const IconComponent = activeToast + ? TOAST_TYPE_TO_ICON[activeToast.type] + : TOAST_TYPE_TO_ICON.default + + const animationStyles = getToastWebAnimationStyles() + return ( <> {activeToast && ( - - - {activeToast.text} + + + + + + {activeToast.text} + = ({}) => { // methods // = -export function show(text: string, icon: FontAwesomeProps['icon'] = 'check') { +export function show( + text: string, + type: ToastType | LegacyToastType = 'default', +) { if (toastTimeout) { clearTimeout(toastTimeout) } - globalSetActiveToast?.({text, icon}) + + globalSetActiveToast?.({text, type: convertLegacyToastType(type)}) toastTimeout = setTimeout(() => { globalSetActiveToast?.(undefined) }, DURATION) @@ -78,12 +149,12 @@ const styles = StyleSheet.create({ bottom: 20, // @ts-ignore web only width: 'calc(100% - 40px)', - maxWidth: 350, + maxWidth: 380, padding: 20, flexDirection: 'row', alignItems: 'center', - backgroundColor: '#000c', borderRadius: 10, + borderWidth: 1, }, dismissBackdrop: { position: 'absolute', @@ -92,13 +163,18 @@ const styles = StyleSheet.create({ bottom: 0, right: 0, }, + iconContainer: { + width: 32, + height: 32, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + }, icon: { - color: '#fff', flexShrink: 0, }, text: { - color: '#fff', - fontSize: 18, marginLeft: 10, }, }) diff --git a/src/view/screens/Storybook/Toasts.tsx b/src/view/screens/Storybook/Toasts.tsx new file mode 100644 index 0000000000..4c17f1c332 --- /dev/null +++ b/src/view/screens/Storybook/Toasts.tsx @@ -0,0 +1,102 @@ +import {Pressable, View} from 'react-native' + +import * as Toast from '#/view/com/util/Toast' +import { + getToastTypeStyles, + TOAST_TYPE_TO_ICON, + type ToastType, +} from '#/view/com/util/Toast.style' +import {atoms as a, useTheme} from '#/alf' +import {H1, Text} from '#/components/Typography' + +function ToastPreview({message, type}: {message: string; type: ToastType}) { + const t = useTheme() + const toastStyles = getToastTypeStyles(t) + const colors = toastStyles[type as keyof typeof toastStyles] + const IconComponent = + TOAST_TYPE_TO_ICON[type as keyof typeof TOAST_TYPE_TO_ICON] + + return ( + Toast.show(message, type)} + style={[ + {backgroundColor: colors.backgroundColor}, + a.shadow_sm, + {borderColor: colors.borderColor}, + a.rounded_sm, + a.border, + a.px_sm, + a.py_sm, + a.flex_row, + a.gap_sm, + a.align_center, + ]}> + + + + + + {message} + + + + ) +} + +export function Toasts() { + return ( + +

Toast Examples

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index a6c2ecdde3..afcc1c4e73 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -20,6 +20,7 @@ import {Settings} from './Settings' import {Shadows} from './Shadows' import {Spacing} from './Spacing' import {Theming} from './Theming' +import {Toasts} from './Toasts' import {Typography} from './Typography' export function Storybook() { @@ -122,6 +123,7 @@ function StorybookInner() { +