Merge branch 'main' into go-124

This commit is contained in:
Jaz Volpert
2025-07-30 17:40:42 +00:00
40 changed files with 1002 additions and 388 deletions
+29 -5
View File
@@ -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= # This is the semver release version of the app, pulled from package.json
SENTRY_AUTH_TOKEN= EXPO_PUBLIC_RELEASE_VERSION=
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG= # This is the commit hash that the current bundle was made from.
EXPO_PUBLIC_BUNDLE_IDENTIFIER= EXPO_PUBLIC_BUNDLE_IDENTIFIER=
# Should be formatted YYMMDDHH so that it increases for each build.
EXPO_PUBLIC_BUNDLE_DATE=0 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=
@@ -43,12 +43,11 @@ jobs:
tags: | tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Set outputs - name: Env
id: vars id: env
run: | run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
- name: Build and push Docker image - name: Build and push Docker image
id: build-and-push id: build-and-push
@@ -62,8 +61,8 @@ jobs:
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
build-args: | build-args: |
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.vars.outputs.sha_short }} EXPO_PUBLIC_ENV=production
SENTRY_DIST=${{ steps.vars.outputs.SENTRY_DIST }} EXPO_PUBLIC_RELEASE_VERSION=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_RELEASE=${{ steps.vars.outputs.SENTRY_RELEASE }} 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_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
+25 -11
View File
@@ -62,23 +62,30 @@ jobs:
- name: Check for i18n compilation errors - 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 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: | run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env 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 "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 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 - 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 - name: ✍️ Rename Testflight bundle
if: ${{ inputs.profile != 'production' }} if: ${{ inputs.profile != 'production' }}
@@ -140,7 +147,14 @@ jobs:
- name: 🏗️ Build Production APK - name: 🏗️ Build Production APK
if: ${{ inputs.profile == 'production' }} 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 - name: 🚀 Upload Production APK Artifact
id: upload-artifact-production-apk id: upload-artifact-production-apk
+16 -9
View File
@@ -75,22 +75,29 @@ jobs:
- name: Check for i18n compilation errors - 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 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 - name: ✏️ Write environment variables
id: env
run: | run: |
echo "${{ secrets.ENV_TOKEN }}" > .env 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 "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 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 - 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 - name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa run: eas submit -p ios --non-interactive --path build.ipa
+49 -18
View File
@@ -101,25 +101,30 @@ jobs:
if: ${{ !steps.fingerprint.outputs.includes-changes }} if: ${{ !steps.fingerprint.outputs.includes-changes }}
uses: dcarbone/install-jq-action@v2 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 }} if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: | run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env 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 "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 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 - name: 🏗️ Create Bundle
if: ${{ !steps.fingerprint.outputs.includes-changes }} 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 - name: 📦 Package Bundle and 🚀 Deploy
if: ${{ !steps.fingerprint.outputs.includes-changes }} if: ${{ !steps.fingerprint.outputs.includes-changes }}
@@ -205,16 +210,29 @@ jobs:
- name: 🔤 Compile translations - name: 🔤 Compile translations
run: yarn intl:build run: yarn intl:build
- name: ✏️ Write environment variables # EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: | run: |
echo "${{ secrets.ENV_TOKEN }}" > .env 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 "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 echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build - 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 - name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa run: eas submit -p ios --non-interactive --path build.ipa
@@ -282,17 +300,30 @@ jobs:
- name: 🔤 Compile translations - name: 🔤 Compile translations
run: yarn intl:build run: yarn intl:build
- name: ✏️ Write environment variables # EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: | run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env 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 "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 echo "$json" > google-services.json
- name: 🏗️ EAS Build - 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 - name: ⏰ Get a timestamp
id: timestamp id: timestamp
+14 -10
View File
@@ -152,23 +152,27 @@ jobs:
- name: 🪛 Setup jq - name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2 uses: dcarbone/install-jq-action@v2
- name: ✏️ Write environment variables - name: Env
id: env
run: | run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}' export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env 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 "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 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 - 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 - name: 📦 Package Bundle and 🚀 Deploy
run: yarn use-build-number bash scripts/bundleUpdate.sh run: yarn use-build-number bash scripts/bundleUpdate.sh
+17 -14
View File
@@ -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 WORKDIR /usr/src/social-app
@@ -19,28 +19,28 @@ ENV GOARCH="amd64"
ENV CGO_ENABLED=1 ENV CGO_ENABLED=1
ENV GOEXPERIMENT="loopvar" 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 # 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 ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev} # If not set by GitHub workflows, we're probably in Render
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-$RENDER_GIT_COMMIT}
# The latest git hash of the preview branch on render.com
ARG RENDER_GIT_COMMIT
# #
# Sentry # Sentry
# #
ARG SENTRY_AUTH_TOKEN ARG SENTRY_AUTH_TOKEN
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown} ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown}
# Will fall back to package.json#version, but this is handled elsewhere ARG EXPO_PUBLIC_SENTRY_DSN
ARG SENTRY_RELEASE ENV EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN
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
# #
# Copy everything into the container # Copy everything into the container
@@ -60,13 +60,16 @@ RUN \. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION && \ nvm install $NODE_VERSION && \
nvm use $NODE_VERSION && \ nvm use $NODE_VERSION && \
echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \ 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_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .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 && \ npm install --global yarn && \
yarn && \ yarn && \
yarn intl:build 2>&1 | tee i18n.log && \ 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 && \ 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 # DEBUG
RUN find ./bskyweb/static && find ./web-build/static RUN find ./bskyweb/static && find ./web-build/static
+1 -1
View File
@@ -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 WORKDIR /usr/src/social-app
+4 -5
View File
@@ -1,11 +1,9 @@
module github.com/bluesky-social/social-app/bskyweb module github.com/bluesky-social/social-app/bskyweb
go 1.23.0 go 1.24.5
toolchain go1.24.5
require ( 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/flosch/pongo2/v6 v6.0.0
github.com/ipfs/go-log v1.0.5 github.com/ipfs/go-log v1.0.5
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
@@ -16,6 +14,8 @@ require (
github.com/urfave/cli/v2 v2.25.7 github.com/urfave/cli/v2 v2.25.7
) )
require github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // 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-multibase v0.2.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-varint v0.0.7 // 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/opentracing/opentracing-go v1.2.0 // indirect
github.com/orandin/slog-gorm v1.3.2 // indirect github.com/orandin/slog-gorm v1.3.2 // indirect
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
+2 -2
View File
@@ -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/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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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-20250729223159-573ae927246a h1:S12KN45uIkRglMHC8PqD/Vsz0+u3KbIaBF/6rit8/Pg=
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/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 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
+2 -2
View File
@@ -89,9 +89,9 @@ If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running
### Adding bitdrift ### 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 ### Adding and Updating Locales
+126 -77
View File
@@ -25,18 +25,17 @@ import {
type ViewStyleProp, type ViewStyleProp,
web, web,
} from '#/alf' } from '#/alf'
import {Button} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard' import * as FeedCard from '#/components/FeedCard'
import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' 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 {InlineLinkText} from '#/components/Link'
import * as ProfileCard from '#/components/ProfileCard' import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
import {ProgressGuideList} from './ProgressGuide/List' import {ProgressGuideList} from './ProgressGuide/List'
const MOBILE_CARD_WIDTH = 300 const MOBILE_CARD_WIDTH = 165
function CardOuter({ function CardOuter({
children, children,
@@ -48,8 +47,8 @@ function CardOuter({
<View <View
style={[ style={[
a.w_full, a.w_full,
a.p_lg, a.p_md,
a.rounded_md, a.rounded_lg,
a.border, a.border,
t.atoms.bg, t.atoms.bg,
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
@@ -65,14 +64,30 @@ function CardOuter({
export function SuggestedFollowPlaceholder() { export function SuggestedFollowPlaceholder() {
const t = useTheme() const t = useTheme()
return (
<CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}>
<ProfileCard.Header>
<ProfileCard.AvatarPlaceholder />
<ProfileCard.NameAndHandlePlaceholder />
</ProfileCard.Header>
<ProfileCard.DescriptionPlaceholder numberOfLines={2} /> return (
<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>
<Button
label=""
size="small"
variant="solid"
color="secondary"
disabled
style={[a.w_full, a.rounded_sm]}>
<ButtonText>Follow</ButtonText>
</Button>
</ProfileCard.Outer>
</CardOuter> </CardOuter>
) )
} }
@@ -243,10 +258,9 @@ export function ProfileGrid({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const navigation = useNavigation<NavigationProp>()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const isLoading = isSuggestionsLoading || !moderationOpts const isLoading = isSuggestionsLoading || !moderationOpts
const maxLength = gtMobile ? 4 : 6 const maxLength = gtMobile ? 3 : 6
const content = isLoading ? ( const content = isLoading ? (
Array(maxLength) Array(maxLength)
@@ -254,7 +268,14 @@ export function ProfileGrid({
.map((_, i) => ( .map((_, i) => (
<View <View
key={i} 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 /> <SuggestedFollowPlaceholder />
</View> </View>
)) ))
@@ -276,44 +297,69 @@ export function ProfileGrid({
}} }}
style={[ style={[
a.flex_1, 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}) => ( {({hovered, pressed}) => (
<CardOuter <CardOuter
style={[ style={[
a.flex_1, a.flex_1,
t.atoms.shadow_sm,
(hovered || pressed) && t.atoms.border_contrast_high, (hovered || pressed) && t.atoms.border_contrast_high,
]}> ]}>
<ProfileCard.Outer> <ProfileCard.Outer>
<ProfileCard.Header> <View
style={[
a.flex_col,
a.align_center,
a.gap_sm,
a.pb_sm,
a.mb_auto,
]}>
<ProfileCard.Avatar <ProfileCard.Avatar
profile={profile} profile={profile}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
size={88}
/> />
<ProfileCard.NameAndHandle <View style={[a.flex_col, a.align_center, a.max_w_full]}>
profile={profile} <ProfileCard.Name
moderationOpts={moderationOpts} profile={profile}
/> moderationOpts={moderationOpts}
<ProfileCard.FollowButton />
profile={profile} <ProfileCard.Description
moderationOpts={moderationOpts} profile={profile}
logContext="FeedInterstitial" numberOfLines={2}
shape="round" style={[
colorInverted t.atoms.text_contrast_medium,
onFollow={() => { a.text_center,
logEvent('suggestedUser:follow', { a.text_xs,
logContext: ]}
viewContext === 'feed' />
? 'InterstitialDiscover' </View>
: 'InterstitialProfile', </View>
location: 'Card',
recId, <ProfileCard.FollowButton
position: index, profile={profile}
}) moderationOpts={moderationOpts}
}} logContext="FeedInterstitial"
/> withIcon={false}
</ProfileCard.Header> style={[a.rounded_sm]}
<ProfileCard.Description profile={profile} numberOfLines={2} /> onFollow={() => {
logEvent('suggestedUser:follow', {
logContext:
viewContext === 'feed'
? 'InterstitialDiscover'
: 'InterstitialProfile',
location: 'Card',
recId,
position: index,
})
}}
/>
</ProfileCard.Outer> </ProfileCard.Outer>
</CardOuter> </CardOuter>
)} )}
@@ -333,36 +379,30 @@ export function ProfileGrid({
<View <View
style={[ style={[
a.p_lg, a.p_lg,
a.pb_xs, a.py_md,
a.flex_row, a.flex_row,
a.align_center, a.align_center,
a.justify_between, 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' ? ( {viewContext === 'profile' ? (
<Trans>Similar accounts</Trans> <Trans>Similar accounts</Trans>
) : ( ) : (
<Trans>Suggested for you</Trans> <Trans>Suggested for you</Trans>
)} )}
</Text> </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> </View>
{gtMobile ? ( {gtMobile ? (
<View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}> <View style={[a.px_lg, a.pb_lg]}>
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}> <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
{content} {content}
</View> </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> </View>
) : ( ) : (
<BlockDrawerGesture> <BlockDrawerGesture>
@@ -371,29 +411,12 @@ export function ProfileGrid({
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap} snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
decelerationRate="fast"> decelerationRate="fast"
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}> style={[a.overflow_visible]}>
<View style={[a.px_lg, a.pb_lg, a.flex_row, a.gap_md]}>
{content} {content}
<Button <SeeMoreSuggestedProfilesCard />
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>
</View> </View>
</ScrollView> </ScrollView>
</View> </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() { export function SuggestedFeeds() {
const numFeedsToDisplay = 3 const numFeedsToDisplay = 3
const t = useTheme() const t = useTheme()
@@ -2,13 +2,13 @@ import {Pressable} from 'react-native'
import * as Clipboard from 'expo-clipboard' import * as Clipboard from 'expo-clipboard'
import {t} from '@lingui/macro' import {t} from '@lingui/macro'
import {IS_INTERNAL} from '#/lib/app-info'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {useGate} from '#/lib/statsig/statsig' import {useGate} from '#/lib/statsig/statsig'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_INTERNAL} from '#/env'
export function DiscoverDebug({ export function DiscoverDebug({
feedContext, feedContext,
@@ -17,7 +17,6 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {IS_INTERNAL} from '#/lib/app-info'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {useOpenLink} from '#/lib/hooks/useOpenLink' import {useOpenLink} from '#/lib/hooks/useOpenLink'
import {getCurrentRoute} from '#/lib/routes/helpers' import {getCurrentRoute} from '#/lib/routes/helpers'
@@ -83,6 +82,7 @@ import {
useReportDialogControl, useReportDialogControl,
} from '#/components/moderation/ReportDialog' } from '#/components/moderation/ReportDialog'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {IS_INTERNAL} from '#/env'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
let PostMenuItems = ({ let PostMenuItems = ({
+36 -9
View File
@@ -20,7 +20,13 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar' 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 { import {
Button, Button,
ButtonIcon, ButtonIcon,
@@ -136,12 +142,14 @@ export function Avatar({
onPress, onPress,
disabledPreview, disabledPreview,
liveOverride, liveOverride,
size = 40,
}: { }: {
profile: bsky.profile.AnyProfileView profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
onPress?: () => void onPress?: () => void
disabledPreview?: boolean disabledPreview?: boolean
liveOverride?: boolean liveOverride?: boolean
size?: number
}) { }) {
const moderation = moderateProfile(profile, moderationOpts) const moderation = moderateProfile(profile, moderationOpts)
@@ -149,7 +157,7 @@ export function Avatar({
return disabledPreview ? ( return disabledPreview ? (
<UserAvatar <UserAvatar
size={40} size={size}
avatar={profile.avatar} avatar={profile.avatar}
type={profile.associated?.labeler ? 'labeler' : 'user'} type={profile.associated?.labeler ? 'labeler' : 'user'}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
@@ -157,7 +165,7 @@ export function Avatar({
/> />
) : ( ) : (
<PreviewableUserAvatar <PreviewableUserAvatar
size={40} size={size}
profile={profile} profile={profile}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
onBeforePress={onPress} onBeforePress={onPress}
@@ -166,7 +174,7 @@ export function Avatar({
) )
} }
export function AvatarPlaceholder() { export function AvatarPlaceholder({size = 40}: {size?: number}) {
const t = useTheme() const t = useTheme()
return ( return (
<View <View
@@ -174,8 +182,8 @@ export function AvatarPlaceholder() {
a.rounded_full, a.rounded_full,
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
{ {
width: 40, width: size,
height: 40, height: size,
}, },
]} ]}
/> />
@@ -274,7 +282,7 @@ export function Name({
) )
const verification = useSimpleVerificationState({profile}) const verification = useSimpleVerificationState({profile})
return ( return (
<View style={[a.flex_row, a.align_center]}> <View style={[a.flex_row, a.align_center, a.max_w_full]}>
<Text <Text
emoji emoji
style={[ 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({ export function Description({
profile: profileUnshadowed, profile: profileUnshadowed,
numberOfLines = 3, numberOfLines = 3,
style,
}: { }: {
profile: bsky.profile.AnyProfileView profile: bsky.profile.AnyProfileView
numberOfLines?: number numberOfLines?: number
}) { } & TextStyleProp) {
const profile = useProfileShadow(profileUnshadowed) const profile = useProfileShadow(profileUnshadowed)
const rt = useMemo(() => { const rt = useMemo(() => {
if (!('description' in profile)) return if (!('description' in profile)) return
@@ -369,7 +396,7 @@ export function Description({
<View style={[a.pt_xs]}> <View style={[a.pt_xs]}>
<RichText <RichText
value={rt} value={rt}
style={[a.leading_snug]} style={[a.leading_snug, style]}
numberOfLines={numberOfLines} numberOfLines={numberOfLines}
disableLinks disableLinks
/> />
-6
View File
@@ -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'
+79
View File
@@ -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
+19
View File
@@ -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'
})`
+15
View File
@@ -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'})`
-18
View File
@@ -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'
})`
-18
View File
@@ -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'})`
+1 -1
View File
@@ -10,9 +10,9 @@ import {
useUpdates, useUpdates,
} from 'expo-updates' } from 'expo-updates'
import {IS_TESTFLIGHT} from '#/lib/app-info'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isIOS} from '#/platform/detection' import {isIOS} from '#/platform/detection'
import {IS_TESTFLIGHT} from '#/env'
const MINIMUM_MINIMIZE_TIME = 15 * 60e3 const MINIMUM_MINIMIZE_TIME = 15 * 60e3
+9 -11
View File
@@ -3,12 +3,11 @@ import {Platform} from 'react-native'
import {AppState, type AppStateStatus} from 'react-native' import {AppState, type AppStateStatus} from 'react-native'
import {Statsig, StatsigProvider} from 'statsig-react-native-expo' import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type MetricEvents} from '#/logger/metrics' import {type MetricEvents} from '#/logger/metrics'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import packageDotJson from '../../../package.json' import * as env from '#/env'
import {useSession} from '../../state/session' import {useSession} from '../../state/session'
import {timeout} from '../async/timeout' import {timeout} from '../async/timeout'
import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback'
@@ -49,12 +48,11 @@ export type {MetricEvents as LogEvents}
function createStatsigOptions(prefetchUsers: StatsigUser[]) { function createStatsigOptions(prefetchUsers: StatsigUser[]) {
return { return {
environment: { environment: {
tier: tier: env.IS_DEV
process.env.NODE_ENV === 'development' ? 'development'
? 'development' : env.IS_TESTFLIGHT
: IS_TESTFLIGHT ? 'staging'
? 'staging' : 'production',
: 'production',
}, },
// Don't block on waiting for network. The fetched config will kick in on next load. // 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. // This ensures the UI is always consistent and doesn't update mid-session.
@@ -212,9 +210,9 @@ function toStatsigUser(did: string | undefined): StatsigUser {
refSrc, refSrc,
refUrl, refUrl,
platform: Platform.OS as 'ios' | 'android' | 'web', platform: Platform.OS as 'ios' | 'android' | 'web',
appVersion: packageDotJson.version, appVersion: env.RELEASE_VERSION,
bundleIdentifier: BUNDLE_IDENTIFIER, bundleIdentifier: env.BUNDLE_IDENTIFIER,
bundleDate: BUNDLE_DATE, bundleDate: env.BUNDLE_DATE,
appLanguage: languagePrefs.appLanguage, appLanguage: languagePrefs.appLanguage,
contentLanguages: languagePrefs.contentLanguages, contentLanguages: languagePrefs.contentLanguages,
}, },
+31 -22
View File
@@ -966,8 +966,8 @@ msgstr ""
#: src/components/hooks/useFollowMethods.ts:35 #: src/components/hooks/useFollowMethods.ts:35
#: src/components/hooks/useFollowMethods.ts:50 #: src/components/hooks/useFollowMethods.ts:50
#: src/components/ProfileCard.tsx:457 #: src/components/ProfileCard.tsx:484
#: src/components/ProfileCard.tsx:478 #: src/components/ProfileCard.tsx:505
#: src/view/com/profile/FollowButton.tsx:38 #: src/view/com/profile/FollowButton.tsx:38
#: src/view/com/profile/FollowButton.tsx:48 #: src/view/com/profile/FollowButton.tsx:48
msgid "An issue occurred, please try again." msgid "An issue occurred, please try again."
@@ -1262,7 +1262,7 @@ msgid "Birthday"
msgstr "" msgstr ""
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:757 #: 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 #: src/view/com/profile/ProfileMenu.tsx:473
msgid "Block" msgid "Block"
msgstr "" msgstr ""
@@ -1423,23 +1423,20 @@ msgstr ""
msgid "Books" msgid "Books"
msgstr "" msgstr ""
#: src/components/FeedInterstitials.tsx:379 #: src/components/FeedInterstitials.tsx:436
msgid "Browse more accounts on the Explore page" msgid "Browse more accounts on the Explore page"
msgstr "" msgstr ""
#: src/components/FeedInterstitials.tsx:517 #: src/components/FeedInterstitials.tsx:566
msgid "Browse more feeds on the Explore page" msgid "Browse more feeds on the Explore page"
msgstr "" msgstr ""
#: src/components/FeedInterstitials.tsx:359 #: src/components/FeedInterstitials.tsx:547
#: src/components/FeedInterstitials.tsx:362 #: src/components/FeedInterstitials.tsx:550
#: src/components/FeedInterstitials.tsx:498
#: src/components/FeedInterstitials.tsx:501
msgid "Browse more suggestions" msgid "Browse more suggestions"
msgstr "" msgstr ""
#: src/components/FeedInterstitials.tsx:387 #: src/components/FeedInterstitials.tsx:575
#: src/components/FeedInterstitials.tsx:526
msgid "Browse more suggestions on the Explore page" msgid "Browse more suggestions on the Explore page"
msgstr "" msgstr ""
@@ -3618,7 +3615,7 @@ msgid "Flexible"
msgstr "" msgstr ""
#. User is not following this account, click to follow #. 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:494
#: src/components/ProfileHoverCard/index.web.tsx:505 #: src/components/ProfileHoverCard/index.web.tsx:505
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:245 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:245
@@ -3696,7 +3693,7 @@ msgid "Followers you know"
msgstr "" msgstr ""
#. User is following this account, click to unfollow #. 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:493
#: src/components/ProfileHoverCard/index.web.tsx:504 #: src/components/ProfileHoverCard/index.web.tsx:504
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:241 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:241
@@ -3711,7 +3708,7 @@ msgctxt "feed-name"
msgid "Following" msgid "Following"
msgstr "" msgstr ""
#: src/components/ProfileCard.tsx:447 #: src/components/ProfileCard.tsx:474
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:89 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:89
msgid "Following {0}" msgid "Following {0}"
msgstr "" msgstr ""
@@ -5459,7 +5456,7 @@ msgstr ""
msgid "No likes yet" msgid "No likes yet"
msgstr "" msgstr ""
#: src/components/ProfileCard.tsx:469 #: src/components/ProfileCard.tsx:496
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:110 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:110
msgid "No longer following {0}" msgid "No longer following {0}"
msgstr "" msgstr ""
@@ -7398,6 +7395,18 @@ msgstr ""
msgid "See jobs at Bluesky" msgid "See jobs at Bluesky"
msgstr "" 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 #: src/view/screens/SavedFeeds.tsx:213
msgid "See this guide" msgid "See this guide"
msgstr "" msgstr ""
@@ -7975,7 +7984,7 @@ msgstr ""
msgid "Signed in as @{0}" msgid "Signed in as @{0}"
msgstr "" msgstr ""
#: src/components/FeedInterstitials.tsx:343 #: src/components/FeedInterstitials.tsx:389
msgid "Similar accounts" msgid "Similar accounts"
msgstr "" msgstr ""
@@ -8005,7 +8014,7 @@ msgstr ""
msgid "Some of your verifications are invalid." msgid "Some of your verifications are invalid."
msgstr "" msgstr ""
#: src/components/FeedInterstitials.tsx:480 #: src/components/FeedInterstitials.tsx:529
msgid "Some other feeds you might like" msgid "Some other feeds you might like"
msgstr "" msgstr ""
@@ -8237,7 +8246,7 @@ msgstr ""
msgid "Suggested Accounts" msgid "Suggested Accounts"
msgstr "" msgstr ""
#: src/components/FeedInterstitials.tsx:345 #: src/components/FeedInterstitials.tsx:391
msgid "Suggested for you" msgid "Suggested for you"
msgstr "" msgstr ""
@@ -8421,7 +8430,7 @@ msgstr ""
msgid "That's everything!" msgid "That's everything!"
msgstr "" msgstr ""
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:315 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:316
#: src/view/com/profile/ProfileMenu.tsx:461 #: src/view/com/profile/ProfileMenu.tsx:461
msgid "The account will be able to interact with you after unblocking." msgid "The account will be able to interact with you after unblocking."
msgstr "" msgstr ""
@@ -9046,7 +9055,7 @@ msgstr ""
#: src/components/dms/MessagesListBlockedFooter.tsx:112 #: src/components/dms/MessagesListBlockedFooter.tsx:112
#: src/components/dms/MessagesListBlockedFooter.tsx:119 #: src/components/dms/MessagesListBlockedFooter.tsx:119
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:203 #: 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/com/profile/ProfileMenu.tsx:473
#: src/view/screens/ProfileList.tsx:723 #: src/view/screens/ProfileList.tsx:723
msgid "Unblock" msgid "Unblock"
@@ -9064,7 +9073,7 @@ msgstr ""
msgid "Unblock account" msgid "Unblock account"
msgstr "" msgstr ""
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:313 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:314
#: src/view/com/profile/ProfileMenu.tsx:455 #: src/view/com/profile/ProfileMenu.tsx:455
msgid "Unblock Account?" msgid "Unblock Account?"
msgstr "" msgstr ""
@@ -9571,7 +9580,7 @@ msgstr ""
msgid "View {0}'s avatar" msgid "View {0}'s avatar"
msgstr "" msgstr ""
#: src/components/ProfileCard.tsx:118 #: src/components/ProfileCard.tsx:124
#: src/screens/Profile/components/ProfileFeedHeader.tsx:454 #: src/screens/Profile/components/ProfileFeedHeader.tsx:454
#: src/screens/Search/components/SearchProfileCard.tsx:36 #: src/screens/Search/components/SearchProfileCard.tsx:36
#: src/screens/VideoFeed/index.tsx:790 #: src/screens/VideoFeed/index.tsx:790
+1 -2
View File
@@ -2,8 +2,7 @@ import {init, SessionStrategy} from '@bitdrift/react-native'
import {Statsig} from 'statsig-react-native-expo' import {Statsig} from 'statsig-react-native-expo'
import {initPromise} from '#/lib/statsig/statsig' import {initPromise} from '#/lib/statsig/statsig'
import {BITDRIFT_API_KEY} from '#/env'
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
initPromise.then(() => { initPromise.then(() => {
let isEnabled = false let isEnabled = false
+2 -1
View File
@@ -14,9 +14,10 @@ import {
} from '#/logger/types' } from '#/logger/types'
import {enabledLogLevels} from '#/logger/util' import {enabledLogLevels} from '#/logger/util'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {ENV} from '#/env'
const TRANSPORTS: Transport[] = (function configureTransports() { const TRANSPORTS: Transport[] = (function configureTransports() {
switch (process.env.NODE_ENV) { switch (ENV) {
case 'production': { case 'production': {
return [sentryTransport, isNative && bitdriftTransport].filter( return [sentryTransport, isNative && bitdriftTransport].filter(
Boolean, Boolean,
+6 -23
View File
@@ -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 {init} from '@sentry/react-native'
import pkgJson from '#/../package.json' import * as env from '#/env'
/**
* 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'
init({ init({
enabled: !__DEV__ && !!process.env.SENTRY_DSN, enabled: !env.IS_DEV && !!env.SENTRY_DSN,
autoSessionTracking: false, 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 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, environment: env.ENV,
dist, dist: env.BUNDLE_IDENTIFIER,
release, release: env.RELEASE_VERSION,
ignoreErrors: [ ignoreErrors: [
/* /*
* Unknown internals errors * Unknown internals errors
+5 -5
View File
@@ -9,7 +9,6 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useMutation} from '@tanstack/react-query' import {useMutation} from '@tanstack/react-query'
import {Statsig} from 'statsig-react-native-expo' import {Statsig} from 'statsig-react-native-expo'
import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info'
import {STATUS_PAGE_URL} from '#/lib/constants' import {STATUS_PAGE_URL} from '#/lib/constants'
import {type CommonNavigatorParams} from '#/lib/routes/types' import {type CommonNavigatorParams} from '#/lib/routes/types'
import {isAndroid, isIOS, isNative} from '#/platform/detection' 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 {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as env from '#/env'
import {useDemoMode} from '#/storage/hooks/demo-mode' import {useDemoMode} from '#/storage/hooks/demo-mode'
import {useDevMode} from '#/storage/hooks/dev-mode' import {useDevMode} from '#/storage/hooks/dev-mode'
import {OTAInfo} from './components/OTAInfo' import {OTAInfo} from './components/OTAInfo'
@@ -123,7 +123,7 @@ export function AboutSettingsScreen({}: Props) {
</SettingsList.PressableItem> </SettingsList.PressableItem>
)} )}
<SettingsList.PressableItem <SettingsList.PressableItem
label={_(msg`Version ${appVersion}`)} label={_(msg`Version ${env.APP_VERSION}`)}
accessibilityHint={_(msg`Copies build version to clipboard`)} accessibilityHint={_(msg`Copies build version to clipboard`)}
onLongPress={() => { onLongPress={() => {
const newDevModeEnabled = !devModeEnabled const newDevModeEnabled = !devModeEnabled
@@ -146,15 +146,15 @@ export function AboutSettingsScreen({}: Props) {
}} }}
onPress={() => { onPress={() => {
setStringAsync( 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`)) Toast.show(_(msg`Copied build version to clipboard`))
}}> }}>
<SettingsList.ItemIcon icon={WrenchIcon} /> <SettingsList.ItemIcon icon={WrenchIcon} />
<SettingsList.ItemText> <SettingsList.ItemText>
<Trans>Version {appVersion}</Trans> <Trans>Version {env.APP_VERSION}</Trans>
</SettingsList.ItemText> </SettingsList.ItemText>
<SettingsList.BadgeText>{bundleInfo}</SettingsList.BadgeText> <SettingsList.BadgeText>{env.APP_METADATA}</SettingsList.BadgeText>
</SettingsList.PressableItem> </SettingsList.PressableItem>
{devModeEnabled && ( {devModeEnabled && (
<> <>
@@ -3,20 +3,20 @@ import {Alert, View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon' 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 {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 {useGate} from '#/lib/statsig/statsig'
import {isAndroid} from '#/platform/detection' import {isAndroid} from '#/platform/detection'
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage' 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 {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_INTERNAL} from '#/env'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
export function AppIconSettingsScreen({}: Props) { export function AppIconSettingsScreen({}: Props) {
+1 -1
View File
@@ -8,7 +8,6 @@ import Animated, {
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {IS_INTERNAL} from '#/lib/app-info'
import { import {
type CommonNavigatorParams, type CommonNavigatorParams,
type NativeStackScreenProps, 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 {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_INTERNAL} from '#/env'
import * as SettingsList from './components/SettingsList' import * as SettingsList from './components/SettingsList'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
+1 -1
View File
@@ -9,7 +9,6 @@ import {useNavigation} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useActorStatus} from '#/lib/actor-status' import {useActorStatus} from '#/lib/actor-status'
import {IS_INTERNAL} from '#/lib/app-info'
import {HELP_DESK_URL} from '#/lib/constants' import {HELP_DESK_URL} from '#/lib/constants'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
@@ -66,6 +65,7 @@ import {
shouldShowVerificationCheckButton, shouldShowVerificationCheckButton,
VerificationCheckButton, VerificationCheckButton,
} from '#/components/verification/VerificationCheckButton' } from '#/components/verification/VerificationCheckButton'
import {IS_INTERNAL} from '#/env'
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
+3 -1
View File
@@ -1,3 +1,5 @@
import {CHAT_PROXY_DID} from '#/env'
export const DM_SERVICE_HEADERS = { export const DM_SERVICE_HEADERS = {
'atproto-proxy': 'did:web:api.bsky.chat#bsky_chat', 'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`,
} }
+5 -5
View File
@@ -1,11 +1,11 @@
import {AtpSessionData, AtpSessionEvent} from '@atproto/api' import {type AtpSessionData, type AtpSessionEvent} from '@atproto/api'
import {sha256} from 'js-sha256' import {sha256} from 'js-sha256'
import {Statsig} from 'statsig-react-native-expo' import {Statsig} from 'statsig-react-native-expo'
import {IS_INTERNAL} from '#/lib/app-info' import {IS_INTERNAL} from '#/env'
import {Schema} from '../persisted' import {type Schema} from '../persisted'
import {Action, State} from './reducer' import {type Action, type State} from './reducer'
import {SessionAccount} from './types' import {type SessionAccount} from './types'
type Reducer = (state: State, action: Action) => State type Reducer = (state: State, action: Action) => State
+8 -14
View File
@@ -101,13 +101,9 @@ export const Link = memo(function Link({
{name: 'activate', label: title}, {name: 'activate', label: title},
] ]
const dataSet = useMemo(() => { const dataSet = anchorNoUnderline
const ds = {...dataSetProp} ? {...dataSetProp, noUnderline: 1}
if (anchorNoUnderline) { : dataSetProp
ds.noUnderline = 1
}
return ds
}, [dataSetProp, anchorNoUnderline])
if (noFeedback) { if (noFeedback) {
return ( return (
@@ -125,6 +121,8 @@ export const Link = memo(function Link({
onAccessibilityAction?.(e) onAccessibilityAction?.(e)
} }
}} }}
// @ts-ignore web only -sfn
dataSet={dataSet}
{...props} {...props}
android_ripple={{ android_ripple={{
color: t.atoms.bg_contrast_25.backgroundColor, color: t.atoms.bg_contrast_25.backgroundColor,
@@ -198,13 +196,9 @@ export const TextLink = memo(function TextLink({
console.error('Unable to detect mismatching label') console.error('Unable to detect mismatching label')
} }
const dataSet = useMemo(() => { const dataSet = anchorNoUnderline
const ds = {...dataSetProp} ? {...dataSetProp, noUnderline: 1}
if (anchorNoUnderline) { : dataSetProp
ds.noUnderline = 1
}
return ds
}, [dataSetProp, anchorNoUnderline])
const onPress = useCallback( const onPress = useCallback(
(e?: Event) => { (e?: Event) => {
+201
View File
@@ -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;
}
}
`
+78 -59
View File
@@ -6,8 +6,8 @@ import {
GestureHandlerRootView, GestureHandlerRootView,
} from 'react-native-gesture-handler' } from 'react-native-gesture-handler'
import Animated, { import Animated, {
FadeInUp, FadeIn,
FadeOutUp, FadeOut,
runOnJS, runOnJS,
useAnimatedReaction, useAnimatedReaction,
useAnimatedStyle, useAnimatedStyle,
@@ -17,37 +17,55 @@ import Animated, {
} from 'react-native-reanimated' } from 'react-native-reanimated'
import RootSiblings from 'react-native-root-siblings' import RootSiblings from 'react-native-root-siblings'
import {useSafeAreaInsets} from 'react-native-safe-area-context' 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 {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 {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
const TIMEOUT = 2e3 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( export function show(
message: string, message: string,
icon: FontAwesomeProps['icon'] = 'check', type: ToastType | LegacyToastType = 'default',
) { ): void {
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
return return
} }
AccessibilityInfo.announceForAccessibility(message) AccessibilityInfo.announceForAccessibility(message)
const item = new RootSiblings( const item = new RootSiblings(
<Toast message={message} icon={icon} destroy={() => item.destroy()} />, (
<Toast
message={message}
type={convertLegacyToastType(type)}
destroy={() => item.destroy()}
/>
),
) )
} }
function Toast({ function Toast({
message, message,
icon, type,
destroy, destroy,
}: { }: {
message: string message: string
icon: FontAwesomeProps['icon'] type: ToastType
destroy: () => void destroy: () => void
}) { }) {
const t = useTheme() const t = useTheme()
@@ -56,6 +74,10 @@ function Toast({
const dismissSwipeTranslateY = useSharedValue(0) const dismissSwipeTranslateY = useSharedValue(0)
const [cardHeight, setCardHeight] = useState(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 // for the exit animation to work on iOS the animated component
// must not be the root component // must not be the root component
// so we need to wrap it in a view and unmount the toast ahead of time // 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"> pointerEvents="box-none">
{alive && ( {alive && (
<Animated.View <Animated.View
entering={FadeInUp} entering={FadeIn.duration(TOAST_ANIMATION_CONFIG.duration)}
exiting={FadeOutUp} exiting={FadeOut.duration(TOAST_ANIMATION_CONFIG.duration * 0.7)}
style={[a.flex_1]}> onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)}
<Animated.View accessibilityRole="alert"
onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)} accessible={true}
accessibilityRole="alert" accessibilityLabel={message}
accessible={true} accessibilityHint=""
accessibilityLabel={message} onAccessibilityEscape={hideAndDestroyImmediately}
accessibilityHint="" style={[
onAccessibilityEscape={hideAndDestroyImmediately} a.flex_1,
style={[ {backgroundColor: colors.backgroundColor},
a.flex_1, a.shadow_sm,
t.name === 'dark' ? t.atoms.bg_contrast_25 : t.atoms.bg, {borderColor: colors.borderColor, borderWidth: 1},
a.shadow_lg, a.rounded_sm,
t.atoms.border_contrast_medium, animatedStyle,
a.rounded_sm, ]}>
a.border, <GestureDetector gesture={panGesture}>
animatedStyle, <View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}>
]}> <View
<GestureDetector gesture={panGesture}> style={[
<View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}> a.flex_shrink_0,
<View a.rounded_full,
style={[ {width: 32, height: 32},
a.flex_shrink_0, a.align_center,
a.rounded_full, a.justify_center,
{width: 32, height: 32}, {
a.align_center, backgroundColor: colors.backgroundColor,
a.justify_center, },
{ ]}>
backgroundColor: <IconComponent fill={colors.iconColor} size="sm" />
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>
</View> </View>
</GestureDetector> <View
</Animated.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>
</View>
</GestureDetector>
</Animated.View> </Animated.View>
)} )}
</GestureHandlerRootView> </GestureHandlerRootView>
+97 -21
View File
@@ -4,17 +4,23 @@
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import {Pressable, StyleSheet, Text, View} from 'react-native' import {Pressable, StyleSheet, Text, View} from 'react-native'
import { import {
FontAwesomeIcon, convertLegacyToastType,
type FontAwesomeIconStyle, getToastTypeStyles,
type Props as FontAwesomeProps, getToastWebAnimationStyles,
} from '@fortawesome/react-native-fontawesome' 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 const DURATION = 3500
interface ActiveToast { interface ActiveToast {
text: string text: string
icon: FontAwesomeProps['icon'] type: ToastType
} }
type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
@@ -28,21 +34,82 @@ let toastTimeout: NodeJS.Timeout | undefined
type ToastContainerProps = {} type ToastContainerProps = {}
export const ToastContainer: React.FC<ToastContainerProps> = ({}) => { export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
const [activeToast, setActiveToast] = useState<ActiveToast | undefined>() const [activeToast, setActiveToast] = useState<ActiveToast | undefined>()
const [isExiting, setIsExiting] = useState(false)
useEffect(() => { useEffect(() => {
globalSetActiveToast = (t: ActiveToast | undefined) => { 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 ( return (
<> <>
{activeToast && ( {activeToast && (
<View style={styles.container}> <View
<FontAwesomeIcon style={[
icon={activeToast.icon} styles.container,
size={20} {
style={styles.icon as FontAwesomeIconStyle} backgroundColor: toastStyles.backgroundColor,
/> borderColor: toastStyles.borderColor,
<Text style={styles.text}>{activeToast.text}</Text> ...(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 <Pressable
style={styles.dismissBackdrop} style={styles.dismissBackdrop}
accessibilityLabel="Dismiss" accessibilityLabel="Dismiss"
@@ -60,11 +127,15 @@ export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
// methods // methods
// = // =
export function show(text: string, icon: FontAwesomeProps['icon'] = 'check') { export function show(
text: string,
type: ToastType | LegacyToastType = 'default',
) {
if (toastTimeout) { if (toastTimeout) {
clearTimeout(toastTimeout) clearTimeout(toastTimeout)
} }
globalSetActiveToast?.({text, icon})
globalSetActiveToast?.({text, type: convertLegacyToastType(type)})
toastTimeout = setTimeout(() => { toastTimeout = setTimeout(() => {
globalSetActiveToast?.(undefined) globalSetActiveToast?.(undefined)
}, DURATION) }, DURATION)
@@ -78,12 +149,12 @@ const styles = StyleSheet.create({
bottom: 20, bottom: 20,
// @ts-ignore web only // @ts-ignore web only
width: 'calc(100% - 40px)', width: 'calc(100% - 40px)',
maxWidth: 350, maxWidth: 380,
padding: 20, padding: 20,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#000c',
borderRadius: 10, borderRadius: 10,
borderWidth: 1,
}, },
dismissBackdrop: { dismissBackdrop: {
position: 'absolute', position: 'absolute',
@@ -92,13 +163,18 @@ const styles = StyleSheet.create({
bottom: 0, bottom: 0,
right: 0, right: 0,
}, },
iconContainer: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
},
icon: { icon: {
color: '#fff',
flexShrink: 0, flexShrink: 0,
}, },
text: { text: {
color: '#fff',
fontSize: 18,
marginLeft: 10, marginLeft: 10,
}, },
}) })
+102
View File
@@ -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>
)
}
+2
View File
@@ -20,6 +20,7 @@ import {Settings} from './Settings'
import {Shadows} from './Shadows' import {Shadows} from './Shadows'
import {Spacing} from './Spacing' import {Spacing} from './Spacing'
import {Theming} from './Theming' import {Theming} from './Theming'
import {Toasts} from './Toasts'
import {Typography} from './Typography' import {Typography} from './Typography'
export function Storybook() { export function Storybook() {
@@ -122,6 +123,7 @@ function StorybookInner() {
<Breakpoints /> <Breakpoints />
<Dialogs /> <Dialogs />
<Admonitions /> <Admonitions />
<Toasts />
<Settings /> <Settings />
<Button <Button
+1 -1
View File
@@ -53,7 +53,7 @@ module.exports = async function (env, argv) {
project: 'app', project: 'app',
authToken: process.env.SENTRY_AUTH_TOKEN, authToken: process.env.SENTRY_AUTH_TOKEN,
release: { release: {
// env is undefined for Render.com builds, fall back // fallback needed for Render.com deployments
name: process.env.SENTRY_RELEASE || version, name: process.env.SENTRY_RELEASE || version,
dist: process.env.SENTRY_DIST, dist: process.env.SENTRY_DIST,
}, },