diff options
author | Jaz Volpert <ericvolp12@gmail.com> | 2025-07-30 17:40:42 +0000 |
---|---|---|
committer | Jaz Volpert <ericvolp12@gmail.com> | 2025-07-30 17:40:42 +0000 |
commit | 13499de325314804d2564c70f0392191d15f0680 (patch) | |
tree | f73c691d15ddc90663accf1cb0b6f50084b15770 | |
parent | 2d23d821d395dd81691f7a1590800e122b59e3c6 (diff) | |
parent | d4b23d3ab4e8448321fecc7bd46b6531ada80348 (diff) | |
download | voidsky-13499de325314804d2564c70f0392191d15f0680.tar.zst |
Merge branch 'main' into go-124
40 files changed, 1001 insertions, 387 deletions
diff --git a/.env.example b/.env.example index a4d21ac33..e18cda4e5 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 399e8669f..6d573b0f7 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 e862a701f..f1e75f87e 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 a197695db..f3759d0dd 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 475bc087f..1bd0b71c4 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 e40eff6c7..d04301002 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 353d1e53b..93b17a86b 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 4a536f70f..7fa8b9ae5 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 0a76e56ee..407ba7d55 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 76a7c4b5b..ffad3e882 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 7817dd095..87b653bdb 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 a92e7be7f..2a3a00ba7 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({ <View style={[ a.w_full, - a.p_lg, - a.rounded_md, + a.p_md, + a.rounded_lg, a.border, t.atoms.bg, t.atoms.border_contrast_low, @@ -65,14 +64,30 @@ function CardOuter({ export function SuggestedFollowPlaceholder() { const t = useTheme() + return ( - <CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}> - <ProfileCard.Header> - <ProfileCard.AvatarPlaceholder /> - <ProfileCard.NameAndHandlePlaceholder /> - </ProfileCard.Header> + <CardOuter + style={[a.gap_md, t.atoms.border_contrast_low, t.atoms.shadow_sm]}> + <ProfileCard.Outer> + <View + style={[a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto]}> + <ProfileCard.AvatarPlaceholder size={88} /> + <ProfileCard.NamePlaceholder /> + <View style={[a.w_full]}> + <ProfileCard.DescriptionPlaceholder numberOfLines={2} /> + </View> + </View> - <ProfileCard.DescriptionPlaceholder numberOfLines={2} /> + <Button + label="" + size="small" + variant="solid" + color="secondary" + disabled + style={[a.w_full, a.rounded_sm]}> + <ButtonText>Follow</ButtonText> + </Button> + </ProfileCard.Outer> </CardOuter> ) } @@ -243,10 +258,9 @@ export function ProfileGrid({ const t = useTheme() const {_} = useLingui() const moderationOpts = useModerationOpts() - const navigation = useNavigation<NavigationProp>() 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) => ( <View key={i} - style={[gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}])]}> + style={[ + gtMobile && + web([ + a.flex_0, + a.flex_grow, + {width: `calc(30% - ${a.gap_md.gap / 2}px)`}, + ]), + ]}> <SuggestedFollowPlaceholder /> </View> )) @@ -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}) => ( <CardOuter style={[ a.flex_1, + t.atoms.shadow_sm, (hovered || pressed) && t.atoms.border_contrast_high, ]}> <ProfileCard.Outer> - <ProfileCard.Header> + <View + style={[ + a.flex_col, + a.align_center, + a.gap_sm, + a.pb_sm, + a.mb_auto, + ]}> <ProfileCard.Avatar profile={profile} moderationOpts={moderationOpts} + size={88} /> - <ProfileCard.NameAndHandle - profile={profile} - moderationOpts={moderationOpts} - /> - <ProfileCard.FollowButton - profile={profile} - moderationOpts={moderationOpts} - logContext="FeedInterstitial" - shape="round" - colorInverted - onFollow={() => { - logEvent('suggestedUser:follow', { - logContext: - viewContext === 'feed' - ? 'InterstitialDiscover' - : 'InterstitialProfile', - location: 'Card', - recId, - position: index, - }) - }} - /> - </ProfileCard.Header> - <ProfileCard.Description profile={profile} numberOfLines={2} /> + <View style={[a.flex_col, a.align_center, a.max_w_full]}> + <ProfileCard.Name + profile={profile} + moderationOpts={moderationOpts} + /> + <ProfileCard.Description + profile={profile} + numberOfLines={2} + style={[ + t.atoms.text_contrast_medium, + a.text_center, + a.text_xs, + ]} + /> + </View> + </View> + + <ProfileCard.FollowButton + profile={profile} + moderationOpts={moderationOpts} + logContext="FeedInterstitial" + withIcon={false} + style={[a.rounded_sm]} + onFollow={() => { + logEvent('suggestedUser:follow', { + logContext: + viewContext === 'feed' + ? 'InterstitialDiscover' + : 'InterstitialProfile', + location: 'Card', + recId, + position: index, + }) + }} + /> </ProfileCard.Outer> </CardOuter> )} @@ -333,36 +379,30 @@ export function ProfileGrid({ <View style={[ a.p_lg, - a.pb_xs, + a.py_md, a.flex_row, a.align_center, a.justify_between, ]}> - <Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}> + <Text style={[a.text_sm, a.font_bold, t.atoms.text]}> {viewContext === 'profile' ? ( <Trans>Similar accounts</Trans> ) : ( <Trans>Suggested for you</Trans> )} </Text> - <Person fill={t.atoms.text_contrast_low.color} size="sm" /> + <InlineLinkText + label={_(msg`See more suggested profiles on the Explore page`)} + to="/search"> + <Trans>See more</Trans> + </InlineLinkText> </View> {gtMobile ? ( - <View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}> - <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}> + <View style={[a.px_lg, a.pb_lg]}> + <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}> {content} </View> - - <View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}> - <InlineLinkText - label={_(msg`Browse more suggestions`)} - to="/search" - style={[t.atoms.text_contrast_medium]}> - <Trans>Browse more suggestions</Trans> - </InlineLinkText> - <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} /> - </View> </View> ) : ( <BlockDrawerGesture> @@ -371,29 +411,12 @@ export function ProfileGrid({ horizontal showsHorizontalScrollIndicator={false} snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap} - decelerationRate="fast"> - <View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}> + decelerationRate="fast" + style={[a.overflow_visible]}> + <View style={[a.px_lg, a.pb_lg, a.flex_row, a.gap_md]}> {content} - <Button - label={_(msg`Browse more accounts on the Explore page`)} - onPress={() => { - navigation.navigate('SearchTab') - }}> - <CardOuter style={[a.flex_1, {borderWidth: 0}]}> - <View style={[a.flex_1, a.justify_center]}> - <View style={[a.flex_row, a.px_lg]}> - <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}> - <Trans> - Browse more suggestions on the Explore page - </Trans> - </Text> - - <Arrow size="xl" /> - </View> - </View> - </CardOuter> - </Button> + <SeeMoreSuggestedProfilesCard /> </View> </ScrollView> </View> @@ -403,6 +426,32 @@ export function ProfileGrid({ ) } +function SeeMoreSuggestedProfilesCard() { + const navigation = useNavigation<NavigationProp>() + const t = useTheme() + const {_} = useLingui() + + return ( + <Button + label={_(msg`Browse more accounts on the Explore page`)} + onPress={() => { + navigation.navigate('SearchTab') + }}> + <CardOuter style={[a.flex_1, t.atoms.shadow_sm]}> + <View style={[a.flex_1, a.justify_center]}> + <View style={[a.flex_col, a.align_center, a.gap_md]}> + <Text style={[a.leading_snug, a.text_center]}> + <Trans>See more accounts you might like</Trans> + </Text> + + <Arrow size="xl" /> + </View> + </View> + </CardOuter> + </Button> + ) +} + export function SuggestedFeeds() { const numFeedsToDisplay = 3 const t = useTheme() diff --git a/src/components/PostControls/DiscoverDebug.tsx b/src/components/PostControls/DiscoverDebug.tsx index 796981f0c..403b50cca 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 f0ef9ed05..ecc3d0174 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 e01c27655..f12d922fd 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 ? ( <UserAvatar - size={40} + size={size} avatar={profile.avatar} type={profile.associated?.labeler ? 'labeler' : 'user'} moderation={moderation.ui('avatar')} @@ -157,7 +165,7 @@ export function Avatar({ /> ) : ( <PreviewableUserAvatar - size={40} + size={size} profile={profile} moderation={moderation.ui('avatar')} onBeforePress={onPress} @@ -166,7 +174,7 @@ export function Avatar({ ) } -export function AvatarPlaceholder() { +export function AvatarPlaceholder({size = 40}: {size?: number}) { const t = useTheme() return ( <View @@ -174,8 +182,8 @@ export function AvatarPlaceholder() { a.rounded_full, t.atoms.bg_contrast_25, { - width: 40, - height: 40, + width: size, + height: size, }, ]} /> @@ -274,7 +282,7 @@ export function Name({ ) const verification = useSimpleVerificationState({profile}) return ( - <View style={[a.flex_row, a.align_center]}> + <View style={[a.flex_row, a.align_center, a.max_w_full]}> <Text emoji style={[ @@ -343,13 +351,32 @@ export function NameAndHandlePlaceholder() { ) } +export function NamePlaceholder({style}: ViewStyleProp) { + const t = useTheme() + + return ( + <View + style={[ + a.rounded_xs, + t.atoms.bg_contrast_25, + { + width: '60%', + height: 14, + }, + style, + ]} + /> + ) +} + 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({ <View style={[a.pt_xs]}> <RichText value={rt} - style={[a.leading_snug]} + style={[a.leading_snug, style]} numberOfLines={numberOfLines} disableLinks /> diff --git a/src/env.ts b/src/env.ts deleted file mode 100644 index 32ce70670..000000000 --- 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 000000000..e68e9fab8 --- /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 000000000..8558c55b5 --- /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 000000000..66087749b --- /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 0749087ea..000000000 --- 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 1530d9976..000000000 --- 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 864d5d697..ba46b6055 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 f2d3ffca9..1091c82e0 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 69532591c..3a5480a5c 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 d6af3fe24..dd2560acc 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 e7aaf666a..998d02581 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 f05a7fc83..d062f05d2 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 0ce127ff3..e48841d9f 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) { </SettingsList.PressableItem> )} <SettingsList.PressableItem - label={_(msg`Version ${appVersion}`)} + label={_(msg`Version ${env.APP_VERSION}`)} accessibilityHint={_(msg`Copies build version to clipboard`)} onLongPress={() => { 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`)) }}> <SettingsList.ItemIcon icon={WrenchIcon} /> <SettingsList.ItemText> - <Trans>Version {appVersion}</Trans> + <Trans>Version {env.APP_VERSION}</Trans> </SettingsList.ItemText> - <SettingsList.BadgeText>{bundleInfo}</SettingsList.BadgeText> + <SettingsList.BadgeText>{env.APP_METADATA}</SettingsList.BadgeText> </SettingsList.PressableItem> {devModeEnabled && ( <> diff --git a/src/screens/Settings/AppIconSettings/index.tsx b/src/screens/Settings/AppIconSettings/index.tsx index 954bac68a..799873c2d 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<CommonNavigatorParams, 'AppIconSettings'> export function AppIconSettingsScreen({}: Props) { diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx index d0158aaa8..492d6d172 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<CommonNavigatorParams, 'AppearanceSettings'> diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index b712c054c..719bbf9a2 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<CommonNavigatorParams, 'Settings'> diff --git a/src/state/queries/messages/const.ts b/src/state/queries/messages/const.ts index 7642c5d7b..1c5519a63 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 98de5a396..bf847f08f 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 6a931d9a4..496b77182 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 000000000..3869e6890 --- /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 56c6780ad..54ef7042d 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( - <Toast message={message} icon={icon} destroy={() => item.destroy()} />, + ( + <Toast + message={message} + type={convertLegacyToastType(type)} + 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 && ( <Animated.View - entering={FadeInUp} - exiting={FadeOutUp} - style={[a.flex_1]}> - <Animated.View - onLayout={evt => 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, - ]}> - <GestureDetector gesture={panGesture}> - <View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}> - <View - style={[ - a.flex_shrink_0, - a.rounded_full, - {width: 32, height: 32}, - a.align_center, - a.justify_center, - { - backgroundColor: - t.name === 'dark' - ? t.palette.black - : t.palette.primary_50, - }, - ]}> - <FontAwesomeIcon - icon={icon} - size={16} - style={t.atoms.text_contrast_medium} - /> - </View> - <View style={[a.h_full, a.justify_center, a.flex_1]}> - <Text style={a.text_md} emoji> - {message} - </Text> - </View> + 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, + ]}> + <GestureDetector gesture={panGesture}> + <View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}> + <View + style={[ + a.flex_shrink_0, + a.rounded_full, + {width: 32, height: 32}, + a.align_center, + a.justify_center, + { + backgroundColor: colors.backgroundColor, + }, + ]}> + <IconComponent fill={colors.iconColor} size="sm" /> + </View> + <View + style={[ + a.h_full, + a.justify_center, + a.flex_1, + a.justify_center, + ]}> + <Text + style={[a.text_md, a.font_bold, {color: colors.textColor}]} + emoji> + {message} + </Text> </View> - </GestureDetector> - </Animated.View> + </View> + </GestureDetector> </Animated.View> )} </GestureHandlerRootView> diff --git a/src/view/com/util/Toast.web.tsx b/src/view/com/util/Toast.web.tsx index d3b7bda33..6b99b30bf 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<ToastContainerProps> = ({}) => { const [activeToast, setActiveToast] = useState<ActiveToast | undefined>() + 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 && ( - <View style={styles.container}> - <FontAwesomeIcon - icon={activeToast.icon} - size={20} - style={styles.icon as FontAwesomeIconStyle} - /> - <Text style={styles.text}>{activeToast.text}</Text> + <View + style={[ + styles.container, + { + backgroundColor: toastStyles.backgroundColor, + borderColor: toastStyles.borderColor, + ...(isExiting + ? animationStyles.exiting + : animationStyles.entering), + }, + ]}> + <View + style={[ + styles.iconContainer, + { + backgroundColor: 'transparent', + }, + ]}> + <IconComponent + fill={toastStyles.iconColor} + size="sm" + style={styles.icon} + /> + </View> + <Text + style={[ + styles.text, + a.text_sm, + a.font_bold, + {color: toastStyles.textColor}, + ]}> + {activeToast.text} + </Text> <Pressable style={styles.dismissBackdrop} accessibilityLabel="Dismiss" @@ -60,11 +127,15 @@ export const ToastContainer: React.FC<ToastContainerProps> = ({}) => { // 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 000000000..4c17f1c33 --- /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 ( + <Pressable + accessibilityRole="button" + onPress={() => 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, + ]}> + <View + style={[ + a.flex_shrink_0, + a.rounded_full, + {width: 24, height: 24}, + a.align_center, + a.justify_center, + { + backgroundColor: colors.backgroundColor, + }, + ]}> + <IconComponent fill={colors.iconColor} size="xs" /> + </View> + <View style={[a.flex_1]}> + <Text + style={[ + a.text_sm, + a.font_bold, + a.leading_snug, + {color: colors.textColor}, + ]} + emoji> + {message} + </Text> + </View> + </Pressable> + ) +} + +export function Toasts() { + return ( + <View style={[a.gap_md]}> + <H1>Toast Examples</H1> + + <View style={[a.gap_md]}> + <View style={[a.gap_xs]}> + <ToastPreview message="Default Toast" type="default" /> + </View> + + <View style={[a.gap_xs]}> + <ToastPreview + message="Operation completed successfully!" + type="success" + /> + </View> + + <View style={[a.gap_xs]}> + <ToastPreview message="Something went wrong!" type="error" /> + </View> + + <View style={[a.gap_xs]}> + <ToastPreview message="Please check your input" type="warning" /> + </View> + + <View style={[a.gap_xs]}> + <ToastPreview message="Here's some helpful information" type="info" /> + </View> + + <View style={[a.gap_xs]}> + <ToastPreview + message="This is a longer message to test how the toast handles multiple lines of text content." + type="info" + /> + </View> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index a6c2ecdde..afcc1c4e7 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() { <Breakpoints /> <Dialogs /> <Admonitions /> + <Toasts /> <Settings /> <Button diff --git a/webpack.config.js b/webpack.config.js index 9a238e549..7f58a7448 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -53,7 +53,7 @@ module.exports = async function (env, argv) { project: 'app', authToken: process.env.SENTRY_AUTH_TOKEN, release: { - // env is undefined for Render.com builds, fall back + // fallback needed for Render.com deployments name: process.env.SENTRY_RELEASE || version, dist: process.env.SENTRY_DIST, }, |