Bluesky app fork with some witchin' additions 💫

Merge branch 'main' into go-124

+29 -5
.env.example
··· 1 - # Copy this to `.env` and `.env.test` files 1 + # The env the app is running in e.g. development, testflight, production 2 + EXPO_PUBLIC_ENV=development 3 + 4 + # This is the semver release version of the app, pulled from package.json 5 + EXPO_PUBLIC_RELEASE_VERSION= 2 6 3 - BITDRIFT_API_KEY= 4 - SENTRY_AUTH_TOKEN= 5 - EXPO_PUBLIC_LOG_LEVEL=debug 6 - EXPO_PUBLIC_LOG_DEBUG= 7 + # This is the commit hash that the current bundle was made from. 7 8 EXPO_PUBLIC_BUNDLE_IDENTIFIER= 9 + 10 + # Should be formatted YYMMDDHH so that it increases for each build. 8 11 EXPO_PUBLIC_BUNDLE_DATE=0 12 + 13 + # The log level for the app's logger transports 14 + EXPO_PUBLIC_LOG_LEVEL=debug 15 + 16 + # Enable debug logs for specific logger instances 17 + EXPO_PUBLIC_LOG_DEBUG=session 18 + 19 + # Chat service DID 20 + EXPO_PUBLIC_CHAT_PROXY_DID= 21 + 22 + # 23 + # 24 + # Bluesky specific values 25 + # 26 + # 27 + 28 + # Sentry DSN for telemetry 29 + EXPO_PUBLIC_SENTRY_DSN= 30 + 31 + # Bitdrift API key. If undefined, Bitdrift will be disabled. 32 + EXPO_PUBLIC_BITDRIFT_API_KEY=
+8 -9
.github/workflows/build-and-push-bskyweb-aws.yaml
··· 43 43 tags: | 44 44 type=sha,enable=true,priority=100,prefix=,suffix=,format=long 45 45 46 - - name: Set outputs 47 - id: vars 46 + - name: Env 47 + id: env 48 48 run: | 49 - echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 50 - echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 51 - echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 49 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 50 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 52 51 53 52 - name: Build and push Docker image 54 53 id: build-and-push ··· 62 61 cache-from: type=gha 63 62 cache-to: type=gha,mode=max 64 63 build-args: | 65 - EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.vars.outputs.sha_short }} 66 - SENTRY_DIST=${{ steps.vars.outputs.SENTRY_DIST }} 67 - SENTRY_RELEASE=${{ steps.vars.outputs.SENTRY_RELEASE }} 64 + EXPO_PUBLIC_ENV=production 65 + EXPO_PUBLIC_RELEASE_VERSION=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} 66 + EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} 67 + EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }} 68 68 SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} 69 - SENTRY_DSN=${{ secrets.SENTRY_DSN }}
+25 -11
.github/workflows/build-submit-android.yml
··· 62 62 - name: Check for i18n compilation errors 63 63 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 64 64 65 - - name: ✏️ Write environment variables 65 + # EXPO_PUBLIC_ENV is handled in eas.json 66 + - name: Env 67 + id: env 66 68 run: | 67 69 export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' 68 70 echo "${{ secrets.ENV_TOKEN }}" > .env 69 - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env 71 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env 72 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 73 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env 74 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 70 75 echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env 71 - echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 76 + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env 77 + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 72 78 echo "$json" > google-services.json 73 79 74 - - name: Setup Sentry vars for build-time injection 75 - id: sentry 76 - run: | 77 - echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 78 - echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 79 - 80 80 - name: 🏗️ EAS Build 81 - 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 81 + run: > 82 + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} 83 + SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} 84 + SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} 85 + yarn use-build-number-with-bump 86 + eas build -p android 87 + --profile ${{ inputs.profile || 'testflight-android' }} 88 + --local --output build.aab --non-interactive 82 89 83 90 - name: ✍️ Rename Testflight bundle 84 91 if: ${{ inputs.profile != 'production' }} ··· 140 147 141 148 - name: 🏗️ Build Production APK 142 149 if: ${{ inputs.profile == 'production' }} 143 - 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 150 + run: > 151 + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} 152 + SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} 153 + SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} 154 + yarn use-build-number-with-bump 155 + eas build -p android 156 + --profile production-apk 157 + --local --output build.apk --non-interactive 144 158 145 159 - name: 🚀 Upload Production APK Artifact 146 160 id: upload-artifact-production-apk
+16 -9
.github/workflows/build-submit-ios.yml
··· 75 75 - name: Check for i18n compilation errors 76 76 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 77 77 78 + # EXPO_PUBLIC_ENV is handled in eas.json 78 79 - name: ✏️ Write environment variables 80 + id: env 79 81 run: | 80 82 echo "${{ secrets.ENV_TOKEN }}" > .env 81 - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env 83 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env 84 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 85 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env 86 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 82 87 echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env 83 - echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 88 + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env 89 + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 84 90 echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json 85 91 86 - - name: Setup Sentry vars for build-time injection 87 - id: sentry 88 - run: | 89 - echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 90 - echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 91 - 92 92 - name: 🏗️ EAS Build 93 - 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 93 + run: > 94 + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} 95 + SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} 96 + SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} 97 + yarn use-build-number-with-bump 98 + eas build -p ios 99 + --profile ${{ inputs.profile || 'testflight' }} 100 + --local --output build.ipa --non-interactive 94 101 95 102 - name: 🚀 Deploy 96 103 run: eas submit -p ios --non-interactive --path build.ipa
+49 -18
.github/workflows/bundle-deploy-eas-update.yml
··· 101 101 if: ${{ !steps.fingerprint.outputs.includes-changes }} 102 102 uses: dcarbone/install-jq-action@v2 103 103 104 - - name: ✏️ Write environment variables 104 + # eas.json not used here, set EXPO_PUBLIC_ENV 105 + - name: Env 106 + id: env 105 107 if: ${{ !steps.fingerprint.outputs.includes-changes }} 106 108 run: | 107 109 export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' 108 110 echo "${{ secrets.ENV_TOKEN }}" > .env 109 - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env 111 + echo "EXPO_PUBLIC_ENV=${{ inputs.channel || 'testflight' }}" >> .env 112 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env 113 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 114 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env 115 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 110 116 echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env 111 - echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 117 + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env 118 + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 112 119 echo "$json" > google-services.json 113 120 114 - - name: Setup Sentry vars for build-time injection 115 - id: sentry 116 - run: | 117 - echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 118 - echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 119 - 120 121 - name: 🏗️ Create Bundle 121 122 if: ${{ !steps.fingerprint.outputs.includes-changes }} 122 - 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 123 + run: > 124 + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} 125 + SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} 126 + SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} 127 + yarn export 123 128 124 129 - name: 📦 Package Bundle and 🚀 Deploy 125 130 if: ${{ !steps.fingerprint.outputs.includes-changes }} ··· 205 210 - name: 🔤 Compile translations 206 211 run: yarn intl:build 207 212 208 - - name: ✏️ Write environment variables 213 + # EXPO_PUBLIC_ENV is handled in eas.json 214 + - name: Env 215 + id: env 209 216 run: | 210 217 echo "${{ secrets.ENV_TOKEN }}" > .env 211 - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env 218 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env 219 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 220 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env 221 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 212 222 echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env 213 - echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 223 + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env 224 + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 214 225 echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json 215 226 216 227 - name: 🏗️ EAS Build 217 - run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive 228 + run: > 229 + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} 230 + SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} 231 + SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} 232 + yarn use-build-number-with-bump 233 + eas build -p ios 234 + --profile testflight 235 + --local --output build.ipa --non-interactive 218 236 219 237 - name: 🚀 Deploy 220 238 run: eas submit -p ios --non-interactive --path build.ipa ··· 282 300 - name: 🔤 Compile translations 283 301 run: yarn intl:build 284 302 285 - - name: ✏️ Write environment variables 303 + # EXPO_PUBLIC_ENV is handled in eas.json 304 + - name: Env 305 + id: env 286 306 run: | 287 307 export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' 288 308 echo "${{ secrets.ENV_TOKEN }}" > .env 289 - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env 309 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env 310 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 311 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env 312 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 290 313 echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env 291 - echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 314 + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env 315 + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 292 316 echo "$json" > google-services.json 293 317 294 318 - name: 🏗️ EAS Build 295 - run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive 319 + run: > 320 + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} 321 + SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} 322 + SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} 323 + yarn use-build-number-with-bump 324 + eas build -p android 325 + --profile testflight-android 326 + --local --output build.apk --non-interactive 296 327 297 328 - name: ⏰ Get a timestamp 298 329 id: timestamp
+14 -10
.github/workflows/pull-request-comment.yml
··· 152 152 - name: 🪛 Setup jq 153 153 uses: dcarbone/install-jq-action@v2 154 154 155 - - name: ✏️ Write environment variables 155 + - name: Env 156 + id: env 156 157 run: | 157 158 export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' 158 159 echo "${{ secrets.ENV_TOKEN }}" > .env 159 - echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env 160 + echo "EXPO_PUBLIC_ENV=testflight" >> .env 161 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env 162 + echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 163 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env 164 + echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 160 165 echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env 161 - echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 166 + echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env 167 + echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env 162 168 echo "$json" > google-services.json 163 169 164 - - name: Setup Sentry vars for build-time injection 165 - id: sentry 166 - run: | 167 - echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 168 - echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 169 - 170 170 - name: 🏗️ Create Bundle 171 - 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 171 + run: > 172 + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} 173 + SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }} 174 + SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }} 175 + yarn export 172 176 173 177 - name: 📦 Package Bundle and 🚀 Deploy 174 178 run: yarn use-build-number bash scripts/bundleUpdate.sh
+17 -14
Dockerfile
··· 1 - FROM golang:1.24-bullseye AS build-env 1 + FROM golang:1.24.5-bullseye AS build-env 2 2 3 3 WORKDIR /usr/src/social-app 4 4 ··· 19 19 ENV CGO_ENABLED=1 20 20 ENV GOEXPERIMENT="loopvar" 21 21 22 + # The latest git hash of the preview branch on render.com 23 + # https://render.com/docs/docker-secrets#environment-variables-in-docker-builds 24 + ARG RENDER_GIT_COMMIT 25 + 22 26 # 23 27 # Expo 24 28 # 29 + ARG EXPO_PUBLIC_ENV 30 + ENV EXPO_PUBLIC_ENV=${EXPO_PUBLIC_ENV:-development} 31 + ARG EXPO_PUBLIC_RELEASE_VERSION 32 + ENV EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION 25 33 ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER 26 - ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev} 27 - 28 - # The latest git hash of the preview branch on render.com 29 - ARG RENDER_GIT_COMMIT 34 + # If not set by GitHub workflows, we're probably in Render 35 + ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-$RENDER_GIT_COMMIT} 30 36 31 37 # 32 38 # Sentry 33 39 # 34 40 ARG SENTRY_AUTH_TOKEN 35 41 ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown} 36 - # Will fall back to package.json#version, but this is handled elsewhere 37 - ARG SENTRY_RELEASE 38 - ENV SENTRY_RELEASE=$SENTRY_RELEASE 39 - ARG SENTRY_DIST 40 - # Default to RENDER_GIT_COMMIT if not set by GitHub workflows 41 - ENV SENTRY_DIST=${SENTRY_DIST:-$RENDER_GIT_COMMIT} 42 - ARG SENTRY_DSN 43 - ENV SENTRY_DSN=$SENTRY_DSN 42 + ARG EXPO_PUBLIC_SENTRY_DSN 43 + ENV EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN 44 44 45 45 # 46 46 # Copy everything into the container ··· 60 60 nvm install $NODE_VERSION && \ 61 61 nvm use $NODE_VERSION && \ 62 62 echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \ 63 + echo "EXPO_PUBLIC_ENV=$EXPO_PUBLIC_ENV" >> .env && \ 64 + echo "EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION" >> .env && \ 63 65 echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \ 64 66 echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env && \ 67 + echo "EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN" >> .env && \ 65 68 npm install --global yarn && \ 66 69 yarn && \ 67 70 yarn intl:build 2>&1 | tee i18n.log && \ 68 71 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 && \ 69 - 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 72 + SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$EXPO_PUBLIC_RELEASE_VERSION SENTRY_DIST=$EXPO_PUBLIC_BUNDLE_IDENTIFIER yarn build-web 70 73 71 74 # DEBUG 72 75 RUN find ./bskyweb/static && find ./web-build/static
+1 -1
Dockerfile.embedr
··· 1 - FROM golang:1.24-bullseye AS build-env 1 + FROM golang:1.24.5-bullseye AS build-env 2 2 3 3 WORKDIR /usr/src/social-app 4 4
+4 -5
bskyweb/go.mod
··· 1 1 module github.com/bluesky-social/social-app/bskyweb 2 2 3 - go 1.23.0 4 - 5 - toolchain go1.24.5 3 + go 1.24.5 6 4 7 5 require ( 8 - github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc 6 + github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a 9 7 github.com/flosch/pongo2/v6 v6.0.0 10 8 github.com/ipfs/go-log v1.0.5 11 9 github.com/joho/godotenv v1.5.1 ··· 15 13 github.com/prometheus/client_golang v1.22.0 16 14 github.com/urfave/cli/v2 v2.25.7 17 15 ) 16 + 17 + require github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 18 18 19 19 require ( 20 20 github.com/beorn7/perks v1.0.1 // indirect ··· 70 70 github.com/multiformats/go-multibase v0.2.0 // indirect 71 71 github.com/multiformats/go-multihash v0.2.3 // indirect 72 72 github.com/multiformats/go-varint v0.0.7 // indirect 73 - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 74 73 github.com/opentracing/opentracing-go v1.2.0 // indirect 75 74 github.com/orandin/slog-gorm v1.3.2 // indirect 76 75 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
+2 -2
bskyweb/go.sum
··· 2 2 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 3 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 4 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 - github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc h1:chbGD59Cn1mw07kbq9Uvb8WUFIr1dcoL5TOhT+I9bV4= 6 - github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng= 5 + github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a h1:S12KN45uIkRglMHC8PqD/Vsz0+u3KbIaBF/6rit8/Pg= 6 + github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q= 7 7 github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= 8 8 github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= 9 9 github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
+2 -2
docs/build.md
··· 89 89 90 90 ### Adding bitdrift 91 91 92 - Adding bitdirft is NOT required. You can keep `BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup. 92 + Adding bitdrift is NOT required. You can keep `EXPO_PUBLIC_BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup. 93 93 94 - 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. 94 + 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. 95 95 96 96 ### Adding and Updating Locales 97 97
+125 -76
src/components/FeedInterstitials.tsx
··· 25 25 type ViewStyleProp, 26 26 web, 27 27 } from '#/alf' 28 - import {Button} from '#/components/Button' 28 + import {Button, ButtonText} from '#/components/Button' 29 29 import * as FeedCard from '#/components/FeedCard' 30 30 import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' 31 31 import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' 32 - import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' 33 32 import {InlineLinkText} from '#/components/Link' 34 33 import * as ProfileCard from '#/components/ProfileCard' 35 34 import {Text} from '#/components/Typography' 36 35 import type * as bsky from '#/types/bsky' 37 36 import {ProgressGuideList} from './ProgressGuide/List' 38 37 39 - const MOBILE_CARD_WIDTH = 300 38 + const MOBILE_CARD_WIDTH = 165 40 39 41 40 function CardOuter({ 42 41 children, ··· 48 47 <View 49 48 style={[ 50 49 a.w_full, 51 - a.p_lg, 52 - a.rounded_md, 50 + a.p_md, 51 + a.rounded_lg, 53 52 a.border, 54 53 t.atoms.bg, 55 54 t.atoms.border_contrast_low, ··· 65 64 66 65 export function SuggestedFollowPlaceholder() { 67 66 const t = useTheme() 67 + 68 68 return ( 69 - <CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}> 70 - <ProfileCard.Header> 71 - <ProfileCard.AvatarPlaceholder /> 72 - <ProfileCard.NameAndHandlePlaceholder /> 73 - </ProfileCard.Header> 69 + <CardOuter 70 + style={[a.gap_md, t.atoms.border_contrast_low, t.atoms.shadow_sm]}> 71 + <ProfileCard.Outer> 72 + <View 73 + style={[a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto]}> 74 + <ProfileCard.AvatarPlaceholder size={88} /> 75 + <ProfileCard.NamePlaceholder /> 76 + <View style={[a.w_full]}> 77 + <ProfileCard.DescriptionPlaceholder numberOfLines={2} /> 78 + </View> 79 + </View> 74 80 75 - <ProfileCard.DescriptionPlaceholder numberOfLines={2} /> 81 + <Button 82 + label="" 83 + size="small" 84 + variant="solid" 85 + color="secondary" 86 + disabled 87 + style={[a.w_full, a.rounded_sm]}> 88 + <ButtonText>Follow</ButtonText> 89 + </Button> 90 + </ProfileCard.Outer> 76 91 </CardOuter> 77 92 ) 78 93 } ··· 243 258 const t = useTheme() 244 259 const {_} = useLingui() 245 260 const moderationOpts = useModerationOpts() 246 - const navigation = useNavigation<NavigationProp>() 247 261 const {gtMobile} = useBreakpoints() 248 262 const isLoading = isSuggestionsLoading || !moderationOpts 249 - const maxLength = gtMobile ? 4 : 6 263 + const maxLength = gtMobile ? 3 : 6 250 264 251 265 const content = isLoading ? ( 252 266 Array(maxLength) ··· 254 268 .map((_, i) => ( 255 269 <View 256 270 key={i} 257 - style={[gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}])]}> 271 + style={[ 272 + gtMobile && 273 + web([ 274 + a.flex_0, 275 + a.flex_grow, 276 + {width: `calc(30% - ${a.gap_md.gap / 2}px)`}, 277 + ]), 278 + ]}> 258 279 <SuggestedFollowPlaceholder /> 259 280 </View> 260 281 )) ··· 276 297 }} 277 298 style={[ 278 299 a.flex_1, 279 - gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}]), 300 + gtMobile && 301 + web([ 302 + a.flex_0, 303 + a.flex_grow, 304 + {width: `calc(30% - ${a.gap_md.gap / 2}px)`}, 305 + ]), 280 306 ]}> 281 307 {({hovered, pressed}) => ( 282 308 <CardOuter 283 309 style={[ 284 310 a.flex_1, 311 + t.atoms.shadow_sm, 285 312 (hovered || pressed) && t.atoms.border_contrast_high, 286 313 ]}> 287 314 <ProfileCard.Outer> 288 - <ProfileCard.Header> 315 + <View 316 + style={[ 317 + a.flex_col, 318 + a.align_center, 319 + a.gap_sm, 320 + a.pb_sm, 321 + a.mb_auto, 322 + ]}> 289 323 <ProfileCard.Avatar 290 324 profile={profile} 291 325 moderationOpts={moderationOpts} 326 + size={88} 292 327 /> 293 - <ProfileCard.NameAndHandle 294 - profile={profile} 295 - moderationOpts={moderationOpts} 296 - /> 297 - <ProfileCard.FollowButton 298 - profile={profile} 299 - moderationOpts={moderationOpts} 300 - logContext="FeedInterstitial" 301 - shape="round" 302 - colorInverted 303 - onFollow={() => { 304 - logEvent('suggestedUser:follow', { 305 - logContext: 306 - viewContext === 'feed' 307 - ? 'InterstitialDiscover' 308 - : 'InterstitialProfile', 309 - location: 'Card', 310 - recId, 311 - position: index, 312 - }) 313 - }} 314 - /> 315 - </ProfileCard.Header> 316 - <ProfileCard.Description profile={profile} numberOfLines={2} /> 328 + <View style={[a.flex_col, a.align_center, a.max_w_full]}> 329 + <ProfileCard.Name 330 + profile={profile} 331 + moderationOpts={moderationOpts} 332 + /> 333 + <ProfileCard.Description 334 + profile={profile} 335 + numberOfLines={2} 336 + style={[ 337 + t.atoms.text_contrast_medium, 338 + a.text_center, 339 + a.text_xs, 340 + ]} 341 + /> 342 + </View> 343 + </View> 344 + 345 + <ProfileCard.FollowButton 346 + profile={profile} 347 + moderationOpts={moderationOpts} 348 + logContext="FeedInterstitial" 349 + withIcon={false} 350 + style={[a.rounded_sm]} 351 + onFollow={() => { 352 + logEvent('suggestedUser:follow', { 353 + logContext: 354 + viewContext === 'feed' 355 + ? 'InterstitialDiscover' 356 + : 'InterstitialProfile', 357 + location: 'Card', 358 + recId, 359 + position: index, 360 + }) 361 + }} 362 + /> 317 363 </ProfileCard.Outer> 318 364 </CardOuter> 319 365 )} ··· 333 379 <View 334 380 style={[ 335 381 a.p_lg, 336 - a.pb_xs, 382 + a.py_md, 337 383 a.flex_row, 338 384 a.align_center, 339 385 a.justify_between, 340 386 ]}> 341 - <Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}> 387 + <Text style={[a.text_sm, a.font_bold, t.atoms.text]}> 342 388 {viewContext === 'profile' ? ( 343 389 <Trans>Similar accounts</Trans> 344 390 ) : ( 345 391 <Trans>Suggested for you</Trans> 346 392 )} 347 393 </Text> 348 - <Person fill={t.atoms.text_contrast_low.color} size="sm" /> 394 + <InlineLinkText 395 + label={_(msg`See more suggested profiles on the Explore page`)} 396 + to="/search"> 397 + <Trans>See more</Trans> 398 + </InlineLinkText> 349 399 </View> 350 400 351 401 {gtMobile ? ( 352 - <View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}> 353 - <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}> 402 + <View style={[a.px_lg, a.pb_lg]}> 403 + <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}> 354 404 {content} 355 405 </View> 356 - 357 - <View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}> 358 - <InlineLinkText 359 - label={_(msg`Browse more suggestions`)} 360 - to="/search" 361 - style={[t.atoms.text_contrast_medium]}> 362 - <Trans>Browse more suggestions</Trans> 363 - </InlineLinkText> 364 - <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} /> 365 - </View> 366 406 </View> 367 407 ) : ( 368 408 <BlockDrawerGesture> ··· 371 411 horizontal 372 412 showsHorizontalScrollIndicator={false} 373 413 snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap} 374 - decelerationRate="fast"> 375 - <View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}> 414 + decelerationRate="fast" 415 + style={[a.overflow_visible]}> 416 + <View style={[a.px_lg, a.pb_lg, a.flex_row, a.gap_md]}> 376 417 {content} 377 418 378 - <Button 379 - label={_(msg`Browse more accounts on the Explore page`)} 380 - onPress={() => { 381 - navigation.navigate('SearchTab') 382 - }}> 383 - <CardOuter style={[a.flex_1, {borderWidth: 0}]}> 384 - <View style={[a.flex_1, a.justify_center]}> 385 - <View style={[a.flex_row, a.px_lg]}> 386 - <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}> 387 - <Trans> 388 - Browse more suggestions on the Explore page 389 - </Trans> 390 - </Text> 391 - 392 - <Arrow size="xl" /> 393 - </View> 394 - </View> 395 - </CardOuter> 396 - </Button> 419 + <SeeMoreSuggestedProfilesCard /> 397 420 </View> 398 421 </ScrollView> 399 422 </View> 400 423 </BlockDrawerGesture> 401 424 )} 402 425 </View> 426 + ) 427 + } 428 + 429 + function SeeMoreSuggestedProfilesCard() { 430 + const navigation = useNavigation<NavigationProp>() 431 + const t = useTheme() 432 + const {_} = useLingui() 433 + 434 + return ( 435 + <Button 436 + label={_(msg`Browse more accounts on the Explore page`)} 437 + onPress={() => { 438 + navigation.navigate('SearchTab') 439 + }}> 440 + <CardOuter style={[a.flex_1, t.atoms.shadow_sm]}> 441 + <View style={[a.flex_1, a.justify_center]}> 442 + <View style={[a.flex_col, a.align_center, a.gap_md]}> 443 + <Text style={[a.leading_snug, a.text_center]}> 444 + <Trans>See more accounts you might like</Trans> 445 + </Text> 446 + 447 + <Arrow size="xl" /> 448 + </View> 449 + </View> 450 + </CardOuter> 451 + </Button> 403 452 ) 404 453 } 405 454
+1 -1
src/components/PostControls/DiscoverDebug.tsx
··· 2 2 import * as Clipboard from 'expo-clipboard' 3 3 import {t} from '@lingui/macro' 4 4 5 - import {IS_INTERNAL} from '#/lib/app-info' 6 5 import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' 7 6 import {useGate} from '#/lib/statsig/statsig' 8 7 import {useSession} from '#/state/session' 9 8 import * as Toast from '#/view/com/util/Toast' 10 9 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 11 10 import {Text} from '#/components/Typography' 11 + import {IS_INTERNAL} from '#/env' 12 12 13 13 export function DiscoverDebug({ 14 14 feedContext,
+1 -1
src/components/PostControls/PostMenu/PostMenuItems.tsx
··· 17 17 import {useLingui} from '@lingui/react' 18 18 import {useNavigation} from '@react-navigation/native' 19 19 20 - import {IS_INTERNAL} from '#/lib/app-info' 21 20 import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' 22 21 import {useOpenLink} from '#/lib/hooks/useOpenLink' 23 22 import {getCurrentRoute} from '#/lib/routes/helpers' ··· 83 82 useReportDialogControl, 84 83 } from '#/components/moderation/ReportDialog' 85 84 import * as Prompt from '#/components/Prompt' 85 + import {IS_INTERNAL} from '#/env' 86 86 import * as bsky from '#/types/bsky' 87 87 88 88 let PostMenuItems = ({
+36 -9
src/components/ProfileCard.tsx
··· 20 20 import {useSession} from '#/state/session' 21 21 import * as Toast from '#/view/com/util/Toast' 22 22 import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar' 23 - import {atoms as a, platform, useTheme} from '#/alf' 23 + import { 24 + atoms as a, 25 + platform, 26 + type TextStyleProp, 27 + useTheme, 28 + type ViewStyleProp, 29 + } from '#/alf' 24 30 import { 25 31 Button, 26 32 ButtonIcon, ··· 136 142 onPress, 137 143 disabledPreview, 138 144 liveOverride, 145 + size = 40, 139 146 }: { 140 147 profile: bsky.profile.AnyProfileView 141 148 moderationOpts: ModerationOpts 142 149 onPress?: () => void 143 150 disabledPreview?: boolean 144 151 liveOverride?: boolean 152 + size?: number 145 153 }) { 146 154 const moderation = moderateProfile(profile, moderationOpts) 147 155 ··· 149 157 150 158 return disabledPreview ? ( 151 159 <UserAvatar 152 - size={40} 160 + size={size} 153 161 avatar={profile.avatar} 154 162 type={profile.associated?.labeler ? 'labeler' : 'user'} 155 163 moderation={moderation.ui('avatar')} ··· 157 165 /> 158 166 ) : ( 159 167 <PreviewableUserAvatar 160 - size={40} 168 + size={size} 161 169 profile={profile} 162 170 moderation={moderation.ui('avatar')} 163 171 onBeforePress={onPress} ··· 166 174 ) 167 175 } 168 176 169 - export function AvatarPlaceholder() { 177 + export function AvatarPlaceholder({size = 40}: {size?: number}) { 170 178 const t = useTheme() 171 179 return ( 172 180 <View ··· 174 182 a.rounded_full, 175 183 t.atoms.bg_contrast_25, 176 184 { 177 - width: 40, 178 - height: 40, 185 + width: size, 186 + height: size, 179 187 }, 180 188 ]} 181 189 /> ··· 274 282 ) 275 283 const verification = useSimpleVerificationState({profile}) 276 284 return ( 277 - <View style={[a.flex_row, a.align_center]}> 285 + <View style={[a.flex_row, a.align_center, a.max_w_full]}> 278 286 <Text 279 287 emoji 280 288 style={[ ··· 343 351 ) 344 352 } 345 353 354 + export function NamePlaceholder({style}: ViewStyleProp) { 355 + const t = useTheme() 356 + 357 + return ( 358 + <View 359 + style={[ 360 + a.rounded_xs, 361 + t.atoms.bg_contrast_25, 362 + { 363 + width: '60%', 364 + height: 14, 365 + }, 366 + style, 367 + ]} 368 + /> 369 + ) 370 + } 371 + 346 372 export function Description({ 347 373 profile: profileUnshadowed, 348 374 numberOfLines = 3, 375 + style, 349 376 }: { 350 377 profile: bsky.profile.AnyProfileView 351 378 numberOfLines?: number 352 - }) { 379 + } & TextStyleProp) { 353 380 const profile = useProfileShadow(profileUnshadowed) 354 381 const rt = useMemo(() => { 355 382 if (!('description' in profile)) return ··· 369 396 <View style={[a.pt_xs]}> 370 397 <RichText 371 398 value={rt} 372 - style={[a.leading_snug]} 399 + style={[a.leading_snug, style]} 373 400 numberOfLines={numberOfLines} 374 401 disableLinks 375 402 />
-6
src/env.ts
··· 1 - export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || '' 2 - export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as 3 - | 'debug' 4 - | 'info' 5 - | 'warn' 6 - | 'error'
+79
src/env/common.ts
··· 1 + import {type Did} from '@atproto/api' 2 + 3 + import packageJson from '#/../package.json' 4 + 5 + /** 6 + * The semver version of the app, as defined in `package.json.` 7 + * 8 + * N.B. The fallback is needed for Render.com deployments 9 + */ 10 + export const RELEASE_VERSION: string = 11 + process.env.EXPO_PUBLIC_RELEASE_VERSION || packageJson.version 12 + 13 + /** 14 + * The env the app is running in e.g. development, testflight, production 15 + */ 16 + export const ENV: string = process.env.EXPO_PUBLIC_ENV 17 + 18 + /** 19 + * Indicates whether the app is running in TestFlight 20 + */ 21 + export const IS_TESTFLIGHT = ENV === 'testflight' 22 + 23 + /** 24 + * Indicates whether the app is __DEV__ 25 + */ 26 + export const IS_DEV = __DEV__ 27 + 28 + /** 29 + * Indicates whether the app is __DEV__ or TestFlight 30 + */ 31 + export const IS_INTERNAL = IS_DEV || IS_TESTFLIGHT 32 + 33 + /** 34 + * The commit hash that the current bundle was made from. The user can 35 + * see the commit hash in the app's settings along with the other version info. 36 + * Useful for debugging/reporting. 37 + */ 38 + export const BUNDLE_IDENTIFIER: string = 39 + process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER || 'dev' 40 + 41 + /** 42 + * This will always be in the format of YYMMDDHH, so that it always increases 43 + * for each build. This should only be used for StatSig reporting and shouldn't 44 + * be used to identify a specific bundle. 45 + */ 46 + export const BUNDLE_DATE: number = !process.env.EXPO_PUBLIC_BUNDLE_DATE 47 + ? 0 48 + : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE) 49 + 50 + /** 51 + * The log level for the app. 52 + */ 53 + export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as 54 + | 'debug' 55 + | 'info' 56 + | 'warn' 57 + | 'error' 58 + 59 + /** 60 + * Enable debug logs for specific logger instances 61 + */ 62 + export const LOG_DEBUG: string = process.env.EXPO_PUBLIC_LOG_DEBUG || '' 63 + 64 + /** 65 + * The DID of the chat service to proxy to 66 + */ 67 + export const CHAT_PROXY_DID: Did = 68 + process.env.EXPO_PUBLIC_CHAT_PROXY_DID || 'did:web:api.bsky.chat' 69 + 70 + /** 71 + * Sentry DSN for telemetry 72 + */ 73 + export const SENTRY_DSN: string | undefined = process.env.EXPO_PUBLIC_SENTRY_DSN 74 + 75 + /** 76 + * Bitdrift API key. If undefined, Bitdrift should be disabled. 77 + */ 78 + export const BITDRIFT_API_KEY: string | undefined = 79 + process.env.EXPO_PUBLIC_BITDRIFT_API_KEY
+19
src/env/index.ts
··· 1 + import {nativeBuildVersion} from 'expo-application' 2 + 3 + import {BUNDLE_IDENTIFIER, IS_TESTFLIGHT, RELEASE_VERSION} from '#/env/common' 4 + 5 + export * from '#/env/common' 6 + 7 + /** 8 + * The semver version of the app, specified in our `package.json`.file. On 9 + * iOs/Android, the native build version is appended to the semver version, so 10 + * that it can be used to identify a specific build. 11 + */ 12 + export const APP_VERSION = `${RELEASE_VERSION}.${nativeBuildVersion}` 13 + 14 + /** 15 + * The short commit hash and environment of the current bundle. 16 + */ 17 + export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${ 18 + __DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod' 19 + })`
+15
src/env/index.web.ts
··· 1 + import {BUNDLE_IDENTIFIER, RELEASE_VERSION} from '#/env/common' 2 + 3 + export * from '#/env/common' 4 + 5 + /** 6 + * The semver version of the app, specified in our `package.json`.file. On 7 + * iOs/Android, the native build version is appended to the semver version, so 8 + * that it can be used to identify a specific build. 9 + */ 10 + export const APP_VERSION = RELEASE_VERSION 11 + 12 + /** 13 + * The short commit hash and environment of the current bundle. 14 + */ 15 + export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${__DEV__ ? 'dev' : 'prod'})`
-18
src/lib/app-info.ts
··· 1 - import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application' 2 - 3 - export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight' 4 - export const IS_INTERNAL = __DEV__ || IS_TESTFLIGHT 5 - 6 - // This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings 7 - // along with the other version info. Useful for debugging/reporting. 8 - export const BUNDLE_IDENTIFIER = process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? '' 9 - 10 - // This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used 11 - // for Statsig reporting and shouldn't be used to identify a specific bundle. 12 - export const BUNDLE_DATE = 13 - IS_TESTFLIGHT || __DEV__ ? 0 : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE) 14 - 15 - export const appVersion = `${nativeApplicationVersion}.${nativeBuildVersion}` 16 - export const bundleInfo = `${BUNDLE_IDENTIFIER} (${ 17 - __DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod' 18 - })`
-18
src/lib/app-info.web.ts
··· 1 - import packageDotJson from '../../package.json' 2 - 3 - export const IS_TESTFLIGHT = false 4 - export const IS_INTERNAL = __DEV__ 5 - 6 - // This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings 7 - // along with the other version info. Useful for debugging/reporting. 8 - export const BUNDLE_IDENTIFIER = 9 - process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? 'dev' 10 - 11 - // This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used 12 - // for Statsig reporting and shouldn't be used to identify a specific bundle. 13 - export const BUNDLE_DATE = __DEV__ 14 - ? 0 15 - : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE) 16 - 17 - export const appVersion = packageDotJson.version 18 - export const bundleInfo = `${BUNDLE_IDENTIFIER} (${__DEV__ ? 'dev' : 'prod'})`
+1 -1
src/lib/hooks/useOTAUpdates.ts
··· 10 10 useUpdates, 11 11 } from 'expo-updates' 12 12 13 - import {IS_TESTFLIGHT} from '#/lib/app-info' 14 13 import {logger} from '#/logger' 15 14 import {isIOS} from '#/platform/detection' 15 + import {IS_TESTFLIGHT} from '#/env' 16 16 17 17 const MINIMUM_MINIMIZE_TIME = 15 * 60e3 18 18
+9 -11
src/lib/statsig/statsig.tsx
··· 3 3 import {AppState, type AppStateStatus} from 'react-native' 4 4 import {Statsig, StatsigProvider} from 'statsig-react-native-expo' 5 5 6 - import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info' 7 6 import {logger} from '#/logger' 8 7 import {type MetricEvents} from '#/logger/metrics' 9 8 import {isWeb} from '#/platform/detection' 10 9 import * as persisted from '#/state/persisted' 11 - import packageDotJson from '../../../package.json' 10 + import * as env from '#/env' 12 11 import {useSession} from '../../state/session' 13 12 import {timeout} from '../async/timeout' 14 13 import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback' ··· 49 48 function createStatsigOptions(prefetchUsers: StatsigUser[]) { 50 49 return { 51 50 environment: { 52 - tier: 53 - process.env.NODE_ENV === 'development' 54 - ? 'development' 55 - : IS_TESTFLIGHT 56 - ? 'staging' 57 - : 'production', 51 + tier: env.IS_DEV 52 + ? 'development' 53 + : env.IS_TESTFLIGHT 54 + ? 'staging' 55 + : 'production', 58 56 }, 59 57 // Don't block on waiting for network. The fetched config will kick in on next load. 60 58 // This ensures the UI is always consistent and doesn't update mid-session. ··· 212 210 refSrc, 213 211 refUrl, 214 212 platform: Platform.OS as 'ios' | 'android' | 'web', 215 - appVersion: packageDotJson.version, 216 - bundleIdentifier: BUNDLE_IDENTIFIER, 217 - bundleDate: BUNDLE_DATE, 213 + appVersion: env.RELEASE_VERSION, 214 + bundleIdentifier: env.BUNDLE_IDENTIFIER, 215 + bundleDate: env.BUNDLE_DATE, 218 216 appLanguage: languagePrefs.appLanguage, 219 217 contentLanguages: languagePrefs.contentLanguages, 220 218 },
+31 -22
src/locale/locales/en/messages.po
··· 966 966 967 967 #: src/components/hooks/useFollowMethods.ts:35 968 968 #: src/components/hooks/useFollowMethods.ts:50 969 - #: src/components/ProfileCard.tsx:457 970 - #: src/components/ProfileCard.tsx:478 969 + #: src/components/ProfileCard.tsx:484 970 + #: src/components/ProfileCard.tsx:505 971 971 #: src/view/com/profile/FollowButton.tsx:38 972 972 #: src/view/com/profile/FollowButton.tsx:48 973 973 msgid "An issue occurred, please try again." ··· 1262 1262 msgstr "" 1263 1263 1264 1264 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:757 1265 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:319 1265 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:320 1266 1266 #: src/view/com/profile/ProfileMenu.tsx:473 1267 1267 msgid "Block" 1268 1268 msgstr "" ··· 1423 1423 msgid "Books" 1424 1424 msgstr "" 1425 1425 1426 - #: src/components/FeedInterstitials.tsx:379 1426 + #: src/components/FeedInterstitials.tsx:436 1427 1427 msgid "Browse more accounts on the Explore page" 1428 1428 msgstr "" 1429 1429 1430 - #: src/components/FeedInterstitials.tsx:517 1430 + #: src/components/FeedInterstitials.tsx:566 1431 1431 msgid "Browse more feeds on the Explore page" 1432 1432 msgstr "" 1433 1433 1434 - #: src/components/FeedInterstitials.tsx:359 1435 - #: src/components/FeedInterstitials.tsx:362 1436 - #: src/components/FeedInterstitials.tsx:498 1437 - #: src/components/FeedInterstitials.tsx:501 1434 + #: src/components/FeedInterstitials.tsx:547 1435 + #: src/components/FeedInterstitials.tsx:550 1438 1436 msgid "Browse more suggestions" 1439 1437 msgstr "" 1440 1438 1441 - #: src/components/FeedInterstitials.tsx:387 1442 - #: src/components/FeedInterstitials.tsx:526 1439 + #: src/components/FeedInterstitials.tsx:575 1443 1440 msgid "Browse more suggestions on the Explore page" 1444 1441 msgstr "" 1445 1442 ··· 3618 3615 msgstr "" 3619 3616 3620 3617 #. User is not following this account, click to follow 3621 - #: src/components/ProfileCard.tsx:490 3618 + #: src/components/ProfileCard.tsx:517 3622 3619 #: src/components/ProfileHoverCard/index.web.tsx:494 3623 3620 #: src/components/ProfileHoverCard/index.web.tsx:505 3624 3621 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:245 ··· 3696 3693 msgstr "" 3697 3694 3698 3695 #. User is following this account, click to unfollow 3699 - #: src/components/ProfileCard.tsx:484 3696 + #: src/components/ProfileCard.tsx:511 3700 3697 #: src/components/ProfileHoverCard/index.web.tsx:493 3701 3698 #: src/components/ProfileHoverCard/index.web.tsx:504 3702 3699 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:241 ··· 3711 3708 msgid "Following" 3712 3709 msgstr "" 3713 3710 3714 - #: src/components/ProfileCard.tsx:447 3711 + #: src/components/ProfileCard.tsx:474 3715 3712 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:89 3716 3713 msgid "Following {0}" 3717 3714 msgstr "" ··· 5459 5456 msgid "No likes yet" 5460 5457 msgstr "" 5461 5458 5462 - #: src/components/ProfileCard.tsx:469 5459 + #: src/components/ProfileCard.tsx:496 5463 5460 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:110 5464 5461 msgid "No longer following {0}" 5465 5462 msgstr "" ··· 7398 7395 msgid "See jobs at Bluesky" 7399 7396 msgstr "" 7400 7397 7398 + #: src/components/FeedInterstitials.tsx:397 7399 + msgid "See more" 7400 + msgstr "" 7401 + 7402 + #: src/components/FeedInterstitials.tsx:444 7403 + msgid "See more accounts you might like" 7404 + msgstr "" 7405 + 7406 + #: src/components/FeedInterstitials.tsx:395 7407 + msgid "See more suggested profiles on the Explore page" 7408 + msgstr "" 7409 + 7401 7410 #: src/view/screens/SavedFeeds.tsx:213 7402 7411 msgid "See this guide" 7403 7412 msgstr "" ··· 7975 7984 msgid "Signed in as @{0}" 7976 7985 msgstr "" 7977 7986 7978 - #: src/components/FeedInterstitials.tsx:343 7987 + #: src/components/FeedInterstitials.tsx:389 7979 7988 msgid "Similar accounts" 7980 7989 msgstr "" 7981 7990 ··· 8005 8014 msgid "Some of your verifications are invalid." 8006 8015 msgstr "" 8007 8016 8008 - #: src/components/FeedInterstitials.tsx:480 8017 + #: src/components/FeedInterstitials.tsx:529 8009 8018 msgid "Some other feeds you might like" 8010 8019 msgstr "" 8011 8020 ··· 8237 8246 msgid "Suggested Accounts" 8238 8247 msgstr "" 8239 8248 8240 - #: src/components/FeedInterstitials.tsx:345 8249 + #: src/components/FeedInterstitials.tsx:391 8241 8250 msgid "Suggested for you" 8242 8251 msgstr "" 8243 8252 ··· 8421 8430 msgid "That's everything!" 8422 8431 msgstr "" 8423 8432 8424 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:315 8433 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:316 8425 8434 #: src/view/com/profile/ProfileMenu.tsx:461 8426 8435 msgid "The account will be able to interact with you after unblocking." 8427 8436 msgstr "" ··· 9046 9055 #: src/components/dms/MessagesListBlockedFooter.tsx:112 9047 9056 #: src/components/dms/MessagesListBlockedFooter.tsx:119 9048 9057 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:203 9049 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:319 9058 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:320 9050 9059 #: src/view/com/profile/ProfileMenu.tsx:473 9051 9060 #: src/view/screens/ProfileList.tsx:723 9052 9061 msgid "Unblock" ··· 9064 9073 msgid "Unblock account" 9065 9074 msgstr "" 9066 9075 9067 - #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:313 9076 + #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:314 9068 9077 #: src/view/com/profile/ProfileMenu.tsx:455 9069 9078 msgid "Unblock Account?" 9070 9079 msgstr "" ··· 9571 9580 msgid "View {0}'s avatar" 9572 9581 msgstr "" 9573 9582 9574 - #: src/components/ProfileCard.tsx:118 9583 + #: src/components/ProfileCard.tsx:124 9575 9584 #: src/screens/Profile/components/ProfileFeedHeader.tsx:454 9576 9585 #: src/screens/Search/components/SearchProfileCard.tsx:36 9577 9586 #: src/screens/VideoFeed/index.tsx:790
+1 -2
src/logger/bitdrift/setup/index.ts
··· 2 2 import {Statsig} from 'statsig-react-native-expo' 3 3 4 4 import {initPromise} from '#/lib/statsig/statsig' 5 - 6 - const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY 5 + import {BITDRIFT_API_KEY} from '#/env' 7 6 8 7 initPromise.then(() => { 9 8 let isEnabled = false
+2 -1
src/logger/index.ts
··· 14 14 } from '#/logger/types' 15 15 import {enabledLogLevels} from '#/logger/util' 16 16 import {isNative} from '#/platform/detection' 17 + import {ENV} from '#/env' 17 18 18 19 const TRANSPORTS: Transport[] = (function configureTransports() { 19 - switch (process.env.NODE_ENV) { 20 + switch (ENV) { 20 21 case 'production': { 21 22 return [sentryTransport, isNative && bitdriftTransport].filter( 22 23 Boolean,
+6 -23
src/logger/sentry/setup/index.ts
··· 1 - /** 2 - * Importing these separately from `platform/detection` and `lib/app-info` to 3 - * avoid future conflicts and/or circular deps 4 - */ 5 - 6 1 import {init} from '@sentry/react-native' 7 2 8 - import pkgJson from '#/../package.json' 9 - 10 - /** 11 - * Examples: 12 - * - `dev` 13 - * - `1.99.0` 14 - */ 15 - const release = process.env.SENTRY_RELEASE || pkgJson.version 16 - 17 - /** 18 - * The latest deployed commit hash 19 - */ 20 - const dist = process.env.SENTRY_DIST || 'dev' 3 + import * as env from '#/env' 21 4 22 5 init({ 23 - enabled: !__DEV__ && !!process.env.SENTRY_DSN, 6 + enabled: !env.IS_DEV && !!env.SENTRY_DSN, 24 7 autoSessionTracking: false, 25 - dsn: process.env.SENTRY_DSN, 8 + dsn: env.SENTRY_DSN, 26 9 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 27 - environment: process.env.NODE_ENV, 28 - dist, 29 - release, 10 + environment: env.ENV, 11 + dist: env.BUNDLE_IDENTIFIER, 12 + release: env.RELEASE_VERSION, 30 13 ignoreErrors: [ 31 14 /* 32 15 * Unknown internals errors
+5 -5
src/screens/Settings/AboutSettings.tsx
··· 9 9 import {useMutation} from '@tanstack/react-query' 10 10 import {Statsig} from 'statsig-react-native-expo' 11 11 12 - import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info' 13 12 import {STATUS_PAGE_URL} from '#/lib/constants' 14 13 import {type CommonNavigatorParams} from '#/lib/routes/types' 15 14 import {isAndroid, isIOS, isNative} from '#/platform/detection' ··· 23 22 import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench' 24 23 import * as Layout from '#/components/Layout' 25 24 import {Loader} from '#/components/Loader' 25 + import * as env from '#/env' 26 26 import {useDemoMode} from '#/storage/hooks/demo-mode' 27 27 import {useDevMode} from '#/storage/hooks/dev-mode' 28 28 import {OTAInfo} from './components/OTAInfo' ··· 123 123 </SettingsList.PressableItem> 124 124 )} 125 125 <SettingsList.PressableItem 126 - label={_(msg`Version ${appVersion}`)} 126 + label={_(msg`Version ${env.APP_VERSION}`)} 127 127 accessibilityHint={_(msg`Copies build version to clipboard`)} 128 128 onLongPress={() => { 129 129 const newDevModeEnabled = !devModeEnabled ··· 146 146 }} 147 147 onPress={() => { 148 148 setStringAsync( 149 - `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`, 149 + `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}`, 150 150 ) 151 151 Toast.show(_(msg`Copied build version to clipboard`)) 152 152 }}> 153 153 <SettingsList.ItemIcon icon={WrenchIcon} /> 154 154 <SettingsList.ItemText> 155 - <Trans>Version {appVersion}</Trans> 155 + <Trans>Version {env.APP_VERSION}</Trans> 156 156 </SettingsList.ItemText> 157 - <SettingsList.BadgeText>{bundleInfo}</SettingsList.BadgeText> 157 + <SettingsList.BadgeText>{env.APP_METADATA}</SettingsList.BadgeText> 158 158 </SettingsList.PressableItem> 159 159 {devModeEnabled && ( 160 160 <>
+4 -4
src/screens/Settings/AppIconSettings/index.tsx
··· 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon' 6 - import {NativeStackScreenProps} from '@react-navigation/native-stack' 6 + import {type NativeStackScreenProps} from '@react-navigation/native-stack' 7 7 8 - import {IS_INTERNAL} from '#/lib/app-info' 9 8 import {PressableScale} from '#/lib/custom-animations/PressableScale' 10 - import {CommonNavigatorParams} from '#/lib/routes/types' 9 + import {type CommonNavigatorParams} from '#/lib/routes/types' 11 10 import {useGate} from '#/lib/statsig/statsig' 12 11 import {isAndroid} from '#/platform/detection' 13 12 import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage' 14 - import {AppIconSet} from '#/screens/Settings/AppIconSettings/types' 13 + import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types' 15 14 import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets' 16 15 import {atoms as a, useTheme} from '#/alf' 17 16 import * as Toggle from '#/components/forms/Toggle' 18 17 import * as Layout from '#/components/Layout' 19 18 import {Text} from '#/components/Typography' 19 + import {IS_INTERNAL} from '#/env' 20 20 21 21 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'> 22 22 export function AppIconSettingsScreen({}: Props) {
+1 -1
src/screens/Settings/AppearanceSettings.tsx
··· 8 8 import {msg, Trans} from '@lingui/macro' 9 9 import {useLingui} from '@lingui/react' 10 10 11 - import {IS_INTERNAL} from '#/lib/app-info' 12 11 import { 13 12 type CommonNavigatorParams, 14 13 type NativeStackScreenProps, ··· 26 25 import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' 27 26 import * as Layout from '#/components/Layout' 28 27 import {Text} from '#/components/Typography' 28 + import {IS_INTERNAL} from '#/env' 29 29 import * as SettingsList from './components/SettingsList' 30 30 31 31 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
+1 -1
src/screens/Settings/Settings.tsx
··· 9 9 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 10 10 11 11 import {useActorStatus} from '#/lib/actor-status' 12 - import {IS_INTERNAL} from '#/lib/app-info' 13 12 import {HELP_DESK_URL} from '#/lib/constants' 14 13 import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 15 14 import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' ··· 66 65 shouldShowVerificationCheckButton, 67 66 VerificationCheckButton, 68 67 } from '#/components/verification/VerificationCheckButton' 68 + import {IS_INTERNAL} from '#/env' 69 69 import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' 70 70 71 71 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
+3 -1
src/state/queries/messages/const.ts
··· 1 + import {CHAT_PROXY_DID} from '#/env' 2 + 1 3 export const DM_SERVICE_HEADERS = { 2 - 'atproto-proxy': 'did:web:api.bsky.chat#bsky_chat', 4 + 'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`, 3 5 }
+5 -5
src/state/session/logging.ts
··· 1 - import {AtpSessionData, AtpSessionEvent} from '@atproto/api' 1 + import {type AtpSessionData, type AtpSessionEvent} from '@atproto/api' 2 2 import {sha256} from 'js-sha256' 3 3 import {Statsig} from 'statsig-react-native-expo' 4 4 5 - import {IS_INTERNAL} from '#/lib/app-info' 6 - import {Schema} from '../persisted' 7 - import {Action, State} from './reducer' 8 - import {SessionAccount} from './types' 5 + import {IS_INTERNAL} from '#/env' 6 + import {type Schema} from '../persisted' 7 + import {type Action, type State} from './reducer' 8 + import {type SessionAccount} from './types' 9 9 10 10 type Reducer = (state: State, action: Action) => State 11 11
+8 -14
src/view/com/util/Link.tsx
··· 101 101 {name: 'activate', label: title}, 102 102 ] 103 103 104 - const dataSet = useMemo(() => { 105 - const ds = {...dataSetProp} 106 - if (anchorNoUnderline) { 107 - ds.noUnderline = 1 108 - } 109 - return ds 110 - }, [dataSetProp, anchorNoUnderline]) 104 + const dataSet = anchorNoUnderline 105 + ? {...dataSetProp, noUnderline: 1} 106 + : dataSetProp 111 107 112 108 if (noFeedback) { 113 109 return ( ··· 125 121 onAccessibilityAction?.(e) 126 122 } 127 123 }} 124 + // @ts-ignore web only -sfn 125 + dataSet={dataSet} 128 126 {...props} 129 127 android_ripple={{ 130 128 color: t.atoms.bg_contrast_25.backgroundColor, ··· 198 196 console.error('Unable to detect mismatching label') 199 197 } 200 198 201 - const dataSet = useMemo(() => { 202 - const ds = {...dataSetProp} 203 - if (anchorNoUnderline) { 204 - ds.noUnderline = 1 205 - } 206 - return ds 207 - }, [dataSetProp, anchorNoUnderline]) 199 + const dataSet = anchorNoUnderline 200 + ? {...dataSetProp, noUnderline: 1} 201 + : dataSetProp 208 202 209 203 const onPress = useCallback( 210 204 (e?: Event) => {
+201
src/view/com/util/Toast.style.tsx
··· 1 + import {select, type Theme} from '#/alf' 2 + import {Check_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check' 3 + import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 4 + import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' 5 + import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 6 + 7 + export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' 8 + 9 + export type LegacyToastType = 10 + | 'xmark' 11 + | 'exclamation-circle' 12 + | 'check' 13 + | 'clipboard-check' 14 + | 'circle-exclamation' 15 + 16 + export const convertLegacyToastType = ( 17 + type: ToastType | LegacyToastType, 18 + ): ToastType => { 19 + switch (type) { 20 + // these ones are fine 21 + case 'default': 22 + case 'success': 23 + case 'error': 24 + case 'warning': 25 + case 'info': 26 + return type 27 + // legacy ones need conversion 28 + case 'xmark': 29 + return 'error' 30 + case 'exclamation-circle': 31 + return 'warning' 32 + case 'check': 33 + return 'success' 34 + case 'clipboard-check': 35 + return 'success' 36 + case 'circle-exclamation': 37 + return 'warning' 38 + default: 39 + return 'default' 40 + } 41 + } 42 + 43 + export const TOAST_ANIMATION_CONFIG = { 44 + duration: 300, 45 + damping: 15, 46 + stiffness: 150, 47 + mass: 0.8, 48 + overshootClamping: false, 49 + restSpeedThreshold: 0.01, 50 + restDisplacementThreshold: 0.01, 51 + } 52 + 53 + export const TOAST_TYPE_TO_ICON = { 54 + default: SuccessIcon, 55 + success: SuccessIcon, 56 + error: ErrorIcon, 57 + warning: WarningIcon, 58 + info: CircleInfo, 59 + } 60 + 61 + export const getToastTypeStyles = (t: Theme) => ({ 62 + default: { 63 + backgroundColor: select(t.name, { 64 + light: t.atoms.bg_contrast_25.backgroundColor, 65 + dim: t.atoms.bg_contrast_100.backgroundColor, 66 + dark: t.atoms.bg_contrast_100.backgroundColor, 67 + }), 68 + borderColor: select(t.name, { 69 + light: t.atoms.border_contrast_low.borderColor, 70 + dim: t.atoms.border_contrast_high.borderColor, 71 + dark: t.atoms.border_contrast_high.borderColor, 72 + }), 73 + iconColor: select(t.name, { 74 + light: t.atoms.text_contrast_medium.color, 75 + dim: t.atoms.text_contrast_medium.color, 76 + dark: t.atoms.text_contrast_medium.color, 77 + }), 78 + textColor: select(t.name, { 79 + light: t.atoms.text_contrast_medium.color, 80 + dim: t.atoms.text_contrast_medium.color, 81 + dark: t.atoms.text_contrast_medium.color, 82 + }), 83 + }, 84 + success: { 85 + backgroundColor: select(t.name, { 86 + light: t.palette.primary_100, 87 + dim: t.palette.primary_100, 88 + dark: t.palette.primary_50, 89 + }), 90 + borderColor: select(t.name, { 91 + light: t.palette.primary_500, 92 + dim: t.palette.primary_500, 93 + dark: t.palette.primary_500, 94 + }), 95 + iconColor: select(t.name, { 96 + light: t.palette.primary_500, 97 + dim: t.palette.primary_600, 98 + dark: t.palette.primary_600, 99 + }), 100 + textColor: select(t.name, { 101 + light: t.palette.primary_500, 102 + dim: t.palette.primary_600, 103 + dark: t.palette.primary_600, 104 + }), 105 + }, 106 + error: { 107 + backgroundColor: select(t.name, { 108 + light: t.palette.negative_200, 109 + dim: t.palette.negative_25, 110 + dark: t.palette.negative_25, 111 + }), 112 + borderColor: select(t.name, { 113 + light: t.palette.negative_300, 114 + dim: t.palette.negative_300, 115 + dark: t.palette.negative_300, 116 + }), 117 + iconColor: select(t.name, { 118 + light: t.palette.negative_600, 119 + dim: t.palette.negative_600, 120 + dark: t.palette.negative_600, 121 + }), 122 + textColor: select(t.name, { 123 + light: t.palette.negative_600, 124 + dim: t.palette.negative_600, 125 + dark: t.palette.negative_600, 126 + }), 127 + }, 128 + warning: { 129 + backgroundColor: select(t.name, { 130 + light: t.atoms.bg_contrast_25.backgroundColor, 131 + dim: t.atoms.bg_contrast_100.backgroundColor, 132 + dark: t.atoms.bg_contrast_100.backgroundColor, 133 + }), 134 + borderColor: select(t.name, { 135 + light: t.atoms.border_contrast_low.borderColor, 136 + dim: t.atoms.border_contrast_high.borderColor, 137 + dark: t.atoms.border_contrast_high.borderColor, 138 + }), 139 + iconColor: select(t.name, { 140 + light: t.atoms.text_contrast_medium.color, 141 + dim: t.atoms.text_contrast_medium.color, 142 + dark: t.atoms.text_contrast_medium.color, 143 + }), 144 + textColor: select(t.name, { 145 + light: t.atoms.text_contrast_medium.color, 146 + dim: t.atoms.text_contrast_medium.color, 147 + dark: t.atoms.text_contrast_medium.color, 148 + }), 149 + }, 150 + info: { 151 + backgroundColor: select(t.name, { 152 + light: t.atoms.bg_contrast_25.backgroundColor, 153 + dim: t.atoms.bg_contrast_100.backgroundColor, 154 + dark: t.atoms.bg_contrast_100.backgroundColor, 155 + }), 156 + borderColor: select(t.name, { 157 + light: t.atoms.border_contrast_low.borderColor, 158 + dim: t.atoms.border_contrast_high.borderColor, 159 + dark: t.atoms.border_contrast_high.borderColor, 160 + }), 161 + iconColor: select(t.name, { 162 + light: t.atoms.text_contrast_medium.color, 163 + dim: t.atoms.text_contrast_medium.color, 164 + dark: t.atoms.text_contrast_medium.color, 165 + }), 166 + textColor: select(t.name, { 167 + light: t.atoms.text_contrast_medium.color, 168 + dim: t.atoms.text_contrast_medium.color, 169 + dark: t.atoms.text_contrast_medium.color, 170 + }), 171 + }, 172 + }) 173 + 174 + export const getToastWebAnimationStyles = () => ({ 175 + entering: { 176 + animation: 'toastFadeIn 0.3s ease-out forwards', 177 + }, 178 + exiting: { 179 + animation: 'toastFadeOut 0.2s ease-in forwards', 180 + }, 181 + }) 182 + 183 + export const TOAST_WEB_KEYFRAMES = ` 184 + @keyframes toastFadeIn { 185 + from { 186 + opacity: 0; 187 + } 188 + to { 189 + opacity: 1; 190 + } 191 + } 192 + 193 + @keyframes toastFadeOut { 194 + from { 195 + opacity: 1; 196 + } 197 + to { 198 + opacity: 0; 199 + } 200 + } 201 + `
+78 -59
src/view/com/util/Toast.tsx
··· 6 6 GestureHandlerRootView, 7 7 } from 'react-native-gesture-handler' 8 8 import Animated, { 9 - FadeInUp, 10 - FadeOutUp, 9 + FadeIn, 10 + FadeOut, 11 11 runOnJS, 12 12 useAnimatedReaction, 13 13 useAnimatedStyle, ··· 17 17 } from 'react-native-reanimated' 18 18 import RootSiblings from 'react-native-root-siblings' 19 19 import {useSafeAreaInsets} from 'react-native-safe-area-context' 20 - import { 21 - FontAwesomeIcon, 22 - type Props as FontAwesomeProps, 23 - } from '@fortawesome/react-native-fontawesome' 24 20 25 21 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 22 + import { 23 + convertLegacyToastType, 24 + getToastTypeStyles, 25 + type LegacyToastType, 26 + TOAST_ANIMATION_CONFIG, 27 + TOAST_TYPE_TO_ICON, 28 + type ToastType, 29 + } from '#/view/com/util/Toast.style' 26 30 import {atoms as a, useTheme} from '#/alf' 27 31 import {Text} from '#/components/Typography' 28 32 29 33 const TIMEOUT = 2e3 30 34 35 + // Use type overloading to mark certain types as deprecated -sfn 36 + // https://stackoverflow.com/a/78325851/13325987 37 + export function show(message: string, type?: ToastType): void 38 + /** 39 + * @deprecated type is deprecated - use one of `'default' | 'success' | 'error' | 'warning' | 'info'` 40 + */ 41 + export function show(message: string, type?: LegacyToastType): void 31 42 export function show( 32 43 message: string, 33 - icon: FontAwesomeProps['icon'] = 'check', 34 - ) { 44 + type: ToastType | LegacyToastType = 'default', 45 + ): void { 35 46 if (process.env.NODE_ENV === 'test') { 36 47 return 37 48 } 49 + 38 50 AccessibilityInfo.announceForAccessibility(message) 39 51 const item = new RootSiblings( 40 - <Toast message={message} icon={icon} destroy={() => item.destroy()} />, 52 + ( 53 + <Toast 54 + message={message} 55 + type={convertLegacyToastType(type)} 56 + destroy={() => item.destroy()} 57 + /> 58 + ), 41 59 ) 42 60 } 43 61 44 62 function Toast({ 45 63 message, 46 - icon, 64 + type, 47 65 destroy, 48 66 }: { 49 67 message: string 50 - icon: FontAwesomeProps['icon'] 68 + type: ToastType 51 69 destroy: () => void 52 70 }) { 53 71 const t = useTheme() ··· 55 73 const isPanning = useSharedValue(false) 56 74 const dismissSwipeTranslateY = useSharedValue(0) 57 75 const [cardHeight, setCardHeight] = useState(0) 76 + 77 + const toastStyles = getToastTypeStyles(t) 78 + const colors = toastStyles[type] 79 + const IconComponent = TOAST_TYPE_TO_ICON[type] 58 80 59 81 // for the exit animation to work on iOS the animated component 60 82 // must not be the root component ··· 159 181 pointerEvents="box-none"> 160 182 {alive && ( 161 183 <Animated.View 162 - entering={FadeInUp} 163 - exiting={FadeOutUp} 164 - style={[a.flex_1]}> 165 - <Animated.View 166 - onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)} 167 - accessibilityRole="alert" 168 - accessible={true} 169 - accessibilityLabel={message} 170 - accessibilityHint="" 171 - onAccessibilityEscape={hideAndDestroyImmediately} 172 - style={[ 173 - a.flex_1, 174 - t.name === 'dark' ? t.atoms.bg_contrast_25 : t.atoms.bg, 175 - a.shadow_lg, 176 - t.atoms.border_contrast_medium, 177 - a.rounded_sm, 178 - a.border, 179 - animatedStyle, 180 - ]}> 181 - <GestureDetector gesture={panGesture}> 182 - <View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}> 183 - <View 184 - style={[ 185 - a.flex_shrink_0, 186 - a.rounded_full, 187 - {width: 32, height: 32}, 188 - a.align_center, 189 - a.justify_center, 190 - { 191 - backgroundColor: 192 - t.name === 'dark' 193 - ? t.palette.black 194 - : t.palette.primary_50, 195 - }, 196 - ]}> 197 - <FontAwesomeIcon 198 - icon={icon} 199 - size={16} 200 - style={t.atoms.text_contrast_medium} 201 - /> 202 - </View> 203 - <View style={[a.h_full, a.justify_center, a.flex_1]}> 204 - <Text style={a.text_md} emoji> 205 - {message} 206 - </Text> 207 - </View> 184 + entering={FadeIn.duration(TOAST_ANIMATION_CONFIG.duration)} 185 + exiting={FadeOut.duration(TOAST_ANIMATION_CONFIG.duration * 0.7)} 186 + onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)} 187 + accessibilityRole="alert" 188 + accessible={true} 189 + accessibilityLabel={message} 190 + accessibilityHint="" 191 + onAccessibilityEscape={hideAndDestroyImmediately} 192 + style={[ 193 + a.flex_1, 194 + {backgroundColor: colors.backgroundColor}, 195 + a.shadow_sm, 196 + {borderColor: colors.borderColor, borderWidth: 1}, 197 + a.rounded_sm, 198 + animatedStyle, 199 + ]}> 200 + <GestureDetector gesture={panGesture}> 201 + <View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}> 202 + <View 203 + style={[ 204 + a.flex_shrink_0, 205 + a.rounded_full, 206 + {width: 32, height: 32}, 207 + a.align_center, 208 + a.justify_center, 209 + { 210 + backgroundColor: colors.backgroundColor, 211 + }, 212 + ]}> 213 + <IconComponent fill={colors.iconColor} size="sm" /> 208 214 </View> 209 - </GestureDetector> 210 - </Animated.View> 215 + <View 216 + style={[ 217 + a.h_full, 218 + a.justify_center, 219 + a.flex_1, 220 + a.justify_center, 221 + ]}> 222 + <Text 223 + style={[a.text_md, a.font_bold, {color: colors.textColor}]} 224 + emoji> 225 + {message} 226 + </Text> 227 + </View> 228 + </View> 229 + </GestureDetector> 211 230 </Animated.View> 212 231 )} 213 232 </GestureHandlerRootView>
+97 -21
src/view/com/util/Toast.web.tsx
··· 4 4 5 5 import {useEffect, useState} from 'react' 6 6 import {Pressable, StyleSheet, Text, View} from 'react-native' 7 + 7 8 import { 8 - FontAwesomeIcon, 9 - type FontAwesomeIconStyle, 10 - type Props as FontAwesomeProps, 11 - } from '@fortawesome/react-native-fontawesome' 9 + convertLegacyToastType, 10 + getToastTypeStyles, 11 + getToastWebAnimationStyles, 12 + type LegacyToastType, 13 + TOAST_TYPE_TO_ICON, 14 + TOAST_WEB_KEYFRAMES, 15 + type ToastType, 16 + } from '#/view/com/util/Toast.style' 17 + import {atoms as a, useTheme} from '#/alf' 12 18 13 19 const DURATION = 3500 14 20 15 21 interface ActiveToast { 16 22 text: string 17 - icon: FontAwesomeProps['icon'] 23 + type: ToastType 18 24 } 19 25 type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void 20 26 ··· 28 34 type ToastContainerProps = {} 29 35 export const ToastContainer: React.FC<ToastContainerProps> = ({}) => { 30 36 const [activeToast, setActiveToast] = useState<ActiveToast | undefined>() 37 + const [isExiting, setIsExiting] = useState(false) 38 + 31 39 useEffect(() => { 32 40 globalSetActiveToast = (t: ActiveToast | undefined) => { 33 - setActiveToast(t) 41 + if (!t && activeToast) { 42 + setIsExiting(true) 43 + setTimeout(() => { 44 + setActiveToast(t) 45 + setIsExiting(false) 46 + }, 200) 47 + } else { 48 + setActiveToast(t) 49 + setIsExiting(false) 50 + } 51 + } 52 + }, [activeToast]) 53 + 54 + useEffect(() => { 55 + const styleId = 'toast-animations' 56 + if (!document.getElementById(styleId)) { 57 + const style = document.createElement('style') 58 + style.id = styleId 59 + style.textContent = TOAST_WEB_KEYFRAMES 60 + document.head.appendChild(style) 34 61 } 35 - }) 62 + }, []) 63 + 64 + const t = useTheme() 65 + 66 + const toastTypeStyles = getToastTypeStyles(t) 67 + const toastStyles = activeToast 68 + ? toastTypeStyles[activeToast.type] 69 + : toastTypeStyles.default 70 + 71 + const IconComponent = activeToast 72 + ? TOAST_TYPE_TO_ICON[activeToast.type] 73 + : TOAST_TYPE_TO_ICON.default 74 + 75 + const animationStyles = getToastWebAnimationStyles() 76 + 36 77 return ( 37 78 <> 38 79 {activeToast && ( 39 - <View style={styles.container}> 40 - <FontAwesomeIcon 41 - icon={activeToast.icon} 42 - size={20} 43 - style={styles.icon as FontAwesomeIconStyle} 44 - /> 45 - <Text style={styles.text}>{activeToast.text}</Text> 80 + <View 81 + style={[ 82 + styles.container, 83 + { 84 + backgroundColor: toastStyles.backgroundColor, 85 + borderColor: toastStyles.borderColor, 86 + ...(isExiting 87 + ? animationStyles.exiting 88 + : animationStyles.entering), 89 + }, 90 + ]}> 91 + <View 92 + style={[ 93 + styles.iconContainer, 94 + { 95 + backgroundColor: 'transparent', 96 + }, 97 + ]}> 98 + <IconComponent 99 + fill={toastStyles.iconColor} 100 + size="sm" 101 + style={styles.icon} 102 + /> 103 + </View> 104 + <Text 105 + style={[ 106 + styles.text, 107 + a.text_sm, 108 + a.font_bold, 109 + {color: toastStyles.textColor}, 110 + ]}> 111 + {activeToast.text} 112 + </Text> 46 113 <Pressable 47 114 style={styles.dismissBackdrop} 48 115 accessibilityLabel="Dismiss" ··· 60 127 // methods 61 128 // = 62 129 63 - export function show(text: string, icon: FontAwesomeProps['icon'] = 'check') { 130 + export function show( 131 + text: string, 132 + type: ToastType | LegacyToastType = 'default', 133 + ) { 64 134 if (toastTimeout) { 65 135 clearTimeout(toastTimeout) 66 136 } 67 - globalSetActiveToast?.({text, icon}) 137 + 138 + globalSetActiveToast?.({text, type: convertLegacyToastType(type)}) 68 139 toastTimeout = setTimeout(() => { 69 140 globalSetActiveToast?.(undefined) 70 141 }, DURATION) ··· 78 149 bottom: 20, 79 150 // @ts-ignore web only 80 151 width: 'calc(100% - 40px)', 81 - maxWidth: 350, 152 + maxWidth: 380, 82 153 padding: 20, 83 154 flexDirection: 'row', 84 155 alignItems: 'center', 85 - backgroundColor: '#000c', 86 156 borderRadius: 10, 157 + borderWidth: 1, 87 158 }, 88 159 dismissBackdrop: { 89 160 position: 'absolute', ··· 92 163 bottom: 0, 93 164 right: 0, 94 165 }, 166 + iconContainer: { 167 + width: 32, 168 + height: 32, 169 + borderRadius: 16, 170 + alignItems: 'center', 171 + justifyContent: 'center', 172 + flexShrink: 0, 173 + }, 95 174 icon: { 96 - color: '#fff', 97 175 flexShrink: 0, 98 176 }, 99 177 text: { 100 - color: '#fff', 101 - fontSize: 18, 102 178 marginLeft: 10, 103 179 }, 104 180 })
+102
src/view/screens/Storybook/Toasts.tsx
··· 1 + import {Pressable, View} from 'react-native' 2 + 3 + import * as Toast from '#/view/com/util/Toast' 4 + import { 5 + getToastTypeStyles, 6 + TOAST_TYPE_TO_ICON, 7 + type ToastType, 8 + } from '#/view/com/util/Toast.style' 9 + import {atoms as a, useTheme} from '#/alf' 10 + import {H1, Text} from '#/components/Typography' 11 + 12 + function ToastPreview({message, type}: {message: string; type: ToastType}) { 13 + const t = useTheme() 14 + const toastStyles = getToastTypeStyles(t) 15 + const colors = toastStyles[type as keyof typeof toastStyles] 16 + const IconComponent = 17 + TOAST_TYPE_TO_ICON[type as keyof typeof TOAST_TYPE_TO_ICON] 18 + 19 + return ( 20 + <Pressable 21 + accessibilityRole="button" 22 + onPress={() => Toast.show(message, type)} 23 + style={[ 24 + {backgroundColor: colors.backgroundColor}, 25 + a.shadow_sm, 26 + {borderColor: colors.borderColor}, 27 + a.rounded_sm, 28 + a.border, 29 + a.px_sm, 30 + a.py_sm, 31 + a.flex_row, 32 + a.gap_sm, 33 + a.align_center, 34 + ]}> 35 + <View 36 + style={[ 37 + a.flex_shrink_0, 38 + a.rounded_full, 39 + {width: 24, height: 24}, 40 + a.align_center, 41 + a.justify_center, 42 + { 43 + backgroundColor: colors.backgroundColor, 44 + }, 45 + ]}> 46 + <IconComponent fill={colors.iconColor} size="xs" /> 47 + </View> 48 + <View style={[a.flex_1]}> 49 + <Text 50 + style={[ 51 + a.text_sm, 52 + a.font_bold, 53 + a.leading_snug, 54 + {color: colors.textColor}, 55 + ]} 56 + emoji> 57 + {message} 58 + </Text> 59 + </View> 60 + </Pressable> 61 + ) 62 + } 63 + 64 + export function Toasts() { 65 + return ( 66 + <View style={[a.gap_md]}> 67 + <H1>Toast Examples</H1> 68 + 69 + <View style={[a.gap_md]}> 70 + <View style={[a.gap_xs]}> 71 + <ToastPreview message="Default Toast" type="default" /> 72 + </View> 73 + 74 + <View style={[a.gap_xs]}> 75 + <ToastPreview 76 + message="Operation completed successfully!" 77 + type="success" 78 + /> 79 + </View> 80 + 81 + <View style={[a.gap_xs]}> 82 + <ToastPreview message="Something went wrong!" type="error" /> 83 + </View> 84 + 85 + <View style={[a.gap_xs]}> 86 + <ToastPreview message="Please check your input" type="warning" /> 87 + </View> 88 + 89 + <View style={[a.gap_xs]}> 90 + <ToastPreview message="Here's some helpful information" type="info" /> 91 + </View> 92 + 93 + <View style={[a.gap_xs]}> 94 + <ToastPreview 95 + message="This is a longer message to test how the toast handles multiple lines of text content." 96 + type="info" 97 + /> 98 + </View> 99 + </View> 100 + </View> 101 + ) 102 + }
+2
src/view/screens/Storybook/index.tsx
··· 20 20 import {Shadows} from './Shadows' 21 21 import {Spacing} from './Spacing' 22 22 import {Theming} from './Theming' 23 + import {Toasts} from './Toasts' 23 24 import {Typography} from './Typography' 24 25 25 26 export function Storybook() { ··· 122 123 <Breakpoints /> 123 124 <Dialogs /> 124 125 <Admonitions /> 126 + <Toasts /> 125 127 <Settings /> 126 128 127 129 <Button
+1 -1
webpack.config.js
··· 53 53 project: 'app', 54 54 authToken: process.env.SENTRY_AUTH_TOKEN, 55 55 release: { 56 - // env is undefined for Render.com builds, fall back 56 + // fallback needed for Render.com deployments 57 57 name: process.env.SENTRY_RELEASE || version, 58 58 dist: process.env.SENTRY_DIST, 59 59 },