Merge remote-tracking branch 'origin/main' into hailey/device-attestation

This commit is contained in:
Hailey
2025-08-01 10:29:44 -07:00
74 changed files with 1602 additions and 1518 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 }}
@@ -3,6 +3,7 @@ on:
push: push:
branches: branches:
- main - main
- echoprom_fix
env: env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
+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.23-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.23-bullseye AS build-env FROM golang:1.24.5-bullseye AS build-env
WORKDIR /usr/src/social-app WORKDIR /usr/src/social-app
+4 -3
View File
@@ -10,7 +10,7 @@ Get the app itself:
## Development Resources ## Development Resources
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also open source, but in [a different git repository](https://github.com/bluesky-social/atproto). This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), which are also open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application. There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application.
@@ -19,7 +19,7 @@ The [Build Instructions](./docs/build.md) are a good place to get started with t
The Authenticated Transfer Protocol ("AT Protocol" or "atproto") is a decentralized social media protocol. You don't *need* to understand AT Protocol to work with this application, but it can help. Learn more at: The Authenticated Transfer Protocol ("AT Protocol" or "atproto") is a decentralized social media protocol. You don't *need* to understand AT Protocol to work with this application, but it can help. Learn more at:
- [Overview and Guides](https://atproto.com/guides/overview) - [Overview and Guides](https://atproto.com/guides/overview)
- [Github Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions - [GitHub Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions
- [Protocol Specifications](https://atproto.com/specs/atp) - [Protocol Specifications](https://atproto.com/specs/atp)
- [Blogpost on self-authenticating data structures](https://bsky.social/about/blog/3-6-2022-a-self-authenticating-social-protocol) - [Blogpost on self-authenticating data structures](https://bsky.social/about/blog/3-6-2022-a-self-authenticating-social-protocol)
@@ -27,6 +27,7 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th
## Contributions ## Contributions
> [!NOTE]
> While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review. > While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review.
**Rules:** **Rules:**
@@ -59,7 +60,7 @@ Please be sure to:
## Security disclosures ## Security disclosures
If you discover any security issues, please send an email to security@bsky.app. The email is automatically CCed to the entire team and we'll respond promptly. If you discover any security issues, please send an email to security@bsky.app. The email is automatically CC'd to the entire team and we'll respond promptly.
## Are you a developer interested in building on atproto? ## Are you a developer interested in building on atproto?
+7 -1
View File
@@ -154,7 +154,13 @@ func serve(cctx *cli.Context) error {
RedirectCode: http.StatusFound, RedirectCode: http.StatusFound,
})) }))
e.Use(echoprometheus.NewMiddleware("")) echoprom := echoprometheus.NewMiddlewareWithConfig(
echoprometheus.MiddlewareConfig{
DoNotUseRequestPathFor404: true,
},
)
e.Use(echoprom)
// //
// configure routes // configure routes
+12 -14
View File
@@ -1,8 +1,6 @@
module github.com/bluesky-social/social-app/bskyweb module github.com/bluesky-social/social-app/bskyweb
go 1.24 go 1.24.5
toolchain go1.24.5
require ( require (
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a
@@ -10,7 +8,9 @@ require (
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
github.com/klauspost/compress v1.18.0 github.com/klauspost/compress v1.18.0
github.com/labstack/echo/v4 v4.13.3 github.com/labstack/echo-contrib v0.17.4
github.com/labstack/echo/v4 v4.13.4
github.com/prometheus/client_golang v1.22.0
github.com/urfave/cli/v2 v2.25.7 github.com/urfave/cli/v2 v2.25.7
) )
@@ -53,7 +53,6 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/labstack/echo-contrib v0.17.4
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
@@ -74,10 +73,9 @@ require (
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
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/asm v1.2.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
@@ -95,12 +93,12 @@ require (
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.38.0 // indirect golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.40.0 // indirect golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.14.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.25.0 // indirect golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.11.0 // indirect golang.org/x/time v0.12.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.6 // indirect
gorm.io/driver/postgres v1.5.7 // indirect gorm.io/driver/postgres v1.5.7 // indirect
+18 -18
View File
@@ -115,8 +115,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk= github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0= github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
@@ -168,10 +168,10 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
@@ -253,8 +253,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -272,16 +272,16 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -297,8 +297,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -310,10 +310,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+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
+1 -1
View File
@@ -50,7 +50,6 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index' import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
@@ -61,6 +60,7 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext' import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {ToastContainer} from '#/components/Toast'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
+270 -296
View File
@@ -14,31 +14,36 @@ import {
View, View,
type ViewStyle, type ViewStyle,
} from 'react-native' } from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
import {atoms as a, flatten, select, tokens, useTheme} from '#/alf' import {atoms as a, flatten, select, useTheme} from '#/alf'
import {type Props as SVGIconProps} from '#/components/icons/common' import {type Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' /**
* The `Button` component, and some extensions of it like `Link` are intended
* to be generic and therefore apply no styles by default. These `VariantProps`
* are what control the `Button`'s presentation, and are intended only use cases where the buttons appear as, well, buttons.
*
* If `Button` or an extension of it are used for other compound components, use this property to avoid misuse of these variant props further down the line.
*
* @example
* type MyComponentProps = Omit<ButtonProps, UninheritableButtonProps> & {...}
*/
export type UninheritableButtonProps = 'variant' | 'color' | 'size' | 'shape'
export type ButtonVariant = 'solid' | 'outline' | 'ghost'
export type ButtonColor = export type ButtonColor =
| 'primary' | 'primary'
| 'secondary' | 'secondary'
| 'secondary_inverted' | 'secondary_inverted'
| 'negative' | 'negative'
| 'negative_secondary' | 'negative_secondary'
| 'gradient_primary'
| 'gradient_sky'
| 'gradient_midnight'
| 'gradient_sunrise'
| 'gradient_sunset'
| 'gradient_nordic'
| 'gradient_bonfire'
export type ButtonSize = 'tiny' | 'small' | 'large' export type ButtonSize = 'tiny' | 'small' | 'large'
export type ButtonShape = 'round' | 'square' | 'default' export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = { export type VariantProps = {
/** /**
* The style variation of the button * The style variation of the button
* @deprecated Use `color` instead.
*/ */
variant?: ButtonVariant variant?: ButtonVariant
/** /**
@@ -131,6 +136,15 @@ export const Button = React.forwardRef<View, ButtonProps>(
}, },
ref, ref,
) => { ) => {
/**
* The `variant` prop is deprecated in favor of simply specifying `color`.
* If a `color` is set, then we want to use the existing codepaths for
* "solid" buttons. This is to maintain backwards compatibility.
*/
if (!variant && color) {
variant = 'solid'
}
const t = useTheme() const t = useTheme()
const [state, setState] = React.useState({ const [state, setState] = React.useState({
pressed: false, pressed: false,
@@ -203,8 +217,13 @@ export const Button = React.forwardRef<View, ButtonProps>(
const baseStyles: ViewStyle[] = [] const baseStyles: ViewStyle[] = []
const hoverStyles: ViewStyle[] = [] const hoverStyles: ViewStyle[] = []
if (color === 'primary') { /*
if (variant === 'solid') { * This is the happy path for new button styles, following the
* deprecation of `variant` prop. This redundant `variant` check is here
* just to make this handling easier to understand.
*/
if (variant === 'solid') {
if (color === 'primary') {
if (!disabled) { if (!disabled) {
baseStyles.push({ baseStyles.push({
backgroundColor: t.palette.primary_500, backgroundColor: t.palette.primary_500,
@@ -221,64 +240,14 @@ export const Button = React.forwardRef<View, ButtonProps>(
}), }),
}) })
} }
} else if (variant === 'outline') { } else if (color === 'secondary') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.primary_500,
})
hoverStyles.push(a.border, {
backgroundColor: t.palette.primary_50,
})
} else {
baseStyles.push(a.border, {
borderColor: t.palette.primary_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.primary_100,
})
}
}
} else if (color === 'secondary') {
if (variant === 'solid') {
if (!disabled) { if (!disabled) {
baseStyles.push(t.atoms.bg_contrast_25) baseStyles.push(t.atoms.bg_contrast_25)
hoverStyles.push(t.atoms.bg_contrast_50) hoverStyles.push(t.atoms.bg_contrast_50)
} else { } else {
baseStyles.push(t.atoms.bg_contrast_100) baseStyles.push(t.atoms.bg_contrast_100)
} }
} else if (variant === 'outline') { } else if (color === 'secondary_inverted') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_300,
})
hoverStyles.push(t.atoms.bg_contrast_50)
} else {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.contrast_25,
})
}
}
} else if (color === 'secondary_inverted') {
if (variant === 'solid') {
if (!disabled) { if (!disabled) {
baseStyles.push({ baseStyles.push({
backgroundColor: t.palette.contrast_900, backgroundColor: t.palette.contrast_900,
@@ -291,31 +260,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
backgroundColor: t.palette.contrast_600, backgroundColor: t.palette.contrast_600,
}) })
} }
} else if (variant === 'outline') { } else if (color === 'negative') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_300,
})
hoverStyles.push(t.atoms.bg_contrast_50)
} else {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.contrast_25,
})
}
}
} else if (color === 'negative') {
if (variant === 'solid') {
if (!disabled) { if (!disabled) {
baseStyles.push({ baseStyles.push({
backgroundColor: t.palette.negative_500, backgroundColor: t.palette.negative_500,
@@ -332,33 +277,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
}), }),
}) })
} }
} else if (variant === 'outline') { } else if (color === 'negative_secondary') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.negative_500,
})
hoverStyles.push(a.border, {
backgroundColor: t.palette.negative_50,
})
} else {
baseStyles.push(a.border, {
borderColor: t.palette.negative_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.negative_100,
})
}
}
} else if (color === 'negative_secondary') {
if (variant === 'solid') {
if (!disabled) { if (!disabled) {
baseStyles.push({ baseStyles.push({
backgroundColor: select(t.name, { backgroundColor: select(t.name, {
@@ -383,31 +302,141 @@ export const Button = React.forwardRef<View, ButtonProps>(
}), }),
}) })
} }
} else if (variant === 'outline') { }
baseStyles.push(a.border, t.atoms.bg, { } else {
borderWidth: 1, /*
}) * BEGIN DEPRECATED STYLES
*/
if (color === 'primary') {
if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) { if (!disabled) {
baseStyles.push(a.border, { baseStyles.push(a.border, {
borderColor: t.palette.negative_500, borderColor: t.palette.primary_500,
}) })
hoverStyles.push(a.border, { hoverStyles.push(a.border, {
backgroundColor: t.palette.negative_50, backgroundColor: t.palette.primary_50,
}) })
} else { } else {
baseStyles.push(a.border, { baseStyles.push(a.border, {
borderColor: t.palette.negative_200, borderColor: t.palette.primary_200,
}) })
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.primary_100,
})
}
} }
} else if (variant === 'ghost') { } else if (color === 'secondary') {
if (!disabled) { if (variant === 'outline') {
baseStyles.push(t.atoms.bg) baseStyles.push(a.border, t.atoms.bg, {
hoverStyles.push({ borderWidth: 1,
backgroundColor: t.palette.negative_100,
}) })
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_300,
})
hoverStyles.push(t.atoms.bg_contrast_50)
} else {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.contrast_25,
})
}
}
} else if (color === 'secondary_inverted') {
if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_300,
})
hoverStyles.push(t.atoms.bg_contrast_50)
} else {
baseStyles.push(a.border, {
borderColor: t.palette.contrast_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.contrast_25,
})
}
}
} else if (color === 'negative') {
if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.negative_500,
})
hoverStyles.push(a.border, {
backgroundColor: t.palette.negative_50,
})
} else {
baseStyles.push(a.border, {
borderColor: t.palette.negative_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.negative_100,
})
}
}
} else if (color === 'negative_secondary') {
if (variant === 'outline') {
baseStyles.push(a.border, t.atoms.bg, {
borderWidth: 1,
})
if (!disabled) {
baseStyles.push(a.border, {
borderColor: t.palette.negative_500,
})
hoverStyles.push(a.border, {
backgroundColor: t.palette.negative_50,
})
} else {
baseStyles.push(a.border, {
borderColor: t.palette.negative_200,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.negative_100,
})
}
} }
} }
/*
* END DEPRECATED STYLES
*/
} }
if (shape === 'default') { if (shape === 'default') {
@@ -471,49 +500,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
} }
}, [t, variant, color, size, shape, disabled]) }, [t, variant, color, size, shape, disabled])
const gradientValues = React.useMemo(() => {
const gradient = {
primary: tokens.gradients.sky,
secondary: tokens.gradients.sky,
secondary_inverted: tokens.gradients.sky,
negative: tokens.gradients.sky,
negative_secondary: tokens.gradients.sky,
gradient_primary: tokens.gradients.primary,
gradient_sky: tokens.gradients.sky,
gradient_midnight: tokens.gradients.midnight,
gradient_sunrise: tokens.gradients.sunrise,
gradient_sunset: tokens.gradients.sunset,
gradient_nordic: tokens.gradients.nordic,
gradient_bonfire: tokens.gradients.bonfire,
}[color || 'primary']
if (variant === 'gradient') {
if (gradient.values.length < 2) {
throw new Error(
'Gradient buttons must have at least two colors in the gradient',
)
}
return {
colors: gradient.values.map(([_, color]) => color) as [
string,
string,
...string[],
],
hoverColors: gradient.values.map(_ => gradient.hover_value) as [
string,
string,
...string[],
],
locations: gradient.values.map(([location, _]) => location) as [
number,
number,
...number[],
],
}
}
}, [variant, color])
const context = React.useMemo<ButtonContext>( const context = React.useMemo<ButtonContext>(
() => ({ () => ({
...state, ...state,
@@ -556,27 +542,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
onHoverOut={onHoverOut} onHoverOut={onHoverOut}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur}> onBlur={onBlur}>
{variant === 'gradient' && gradientValues && (
<View
style={[
a.absolute,
a.inset_0,
a.overflow_hidden,
{borderRadius: flattenedBaseStyles.borderRadius},
]}>
<LinearGradient
colors={
state.hovered || state.pressed
? gradientValues.hoverColors
: gradientValues.colors
}
locations={gradientValues.locations}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[a.absolute, a.inset_0]}
/>
</View>
)}
<Context.Provider value={context}> <Context.Provider value={context}>
{typeof children === 'function' ? children(context) : children} {typeof children === 'function' ? children(context) : children}
</Context.Provider> </Context.Provider>
@@ -592,30 +557,19 @@ export function useSharedButtonTextStyles() {
return React.useMemo(() => { return React.useMemo(() => {
const baseStyles: TextStyle[] = [] const baseStyles: TextStyle[] = []
if (color === 'primary') { /*
if (variant === 'solid') { * This is the happy path for new button styles, following the
* deprecation of `variant` prop. This redundant `variant` check is here
* just to make this handling easier to understand.
*/
if (variant === 'solid') {
if (color === 'primary') {
if (!disabled) { if (!disabled) {
baseStyles.push({color: t.palette.white}) baseStyles.push({color: t.palette.white})
} else { } else {
baseStyles.push({color: t.palette.white, opacity: 0.5}) baseStyles.push({color: t.palette.white, opacity: 0.5})
} }
} else if (variant === 'outline') { } else if (color === 'secondary') {
if (!disabled) {
baseStyles.push({
color: t.palette.primary_600,
})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.primary_600})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
}
} else if (color === 'secondary') {
if (variant === 'solid' || variant === 'gradient') {
if (!disabled) { if (!disabled) {
baseStyles.push({ baseStyles.push({
color: t.palette.contrast_700, color: t.palette.contrast_700,
@@ -625,29 +579,7 @@ export function useSharedButtonTextStyles() {
color: t.palette.contrast_400, color: t.palette.contrast_400,
}) })
} }
} else if (variant === 'outline') { } else if (color === 'secondary_inverted') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
}
} else if (color === 'secondary_inverted') {
if (variant === 'solid' || variant === 'gradient') {
if (!disabled) { if (!disabled) {
baseStyles.push({ baseStyles.push({
color: t.palette.contrast_50, color: t.palette.contrast_50,
@@ -657,49 +589,13 @@ export function useSharedButtonTextStyles() {
color: t.palette.contrast_400, color: t.palette.contrast_400,
}) })
} }
} else if (variant === 'outline') { } else if (color === 'negative') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
}
} else if (color === 'negative') {
if (variant === 'solid' || variant === 'gradient') {
if (!disabled) { if (!disabled) {
baseStyles.push({color: t.palette.white}) baseStyles.push({color: t.palette.white})
} else { } else {
baseStyles.push({color: t.palette.white, opacity: 0.5}) baseStyles.push({color: t.palette.white, opacity: 0.5})
} }
} else if (variant === 'outline') { } else if (color === 'negative_secondary') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
}
} else if (color === 'negative_secondary') {
if (variant === 'solid' || variant === 'gradient') {
if (!disabled) { if (!disabled) {
baseStyles.push({ baseStyles.push({
color: select(t.name, { color: select(t.name, {
@@ -718,25 +614,103 @@ export function useSharedButtonTextStyles() {
opacity: 0.5, opacity: 0.5,
}) })
} }
} else if (variant === 'outline') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
} }
} else { } else {
if (!disabled) { /*
baseStyles.push({color: t.palette.white}) * BEGIN DEPRECATED STYLES
} else { */
baseStyles.push({color: t.palette.white, opacity: 0.5}) if (color === 'primary') {
if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: t.palette.primary_600,
})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.primary_600})
} else {
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
}
}
} else if (color === 'secondary') {
if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
}
} else if (color === 'secondary_inverted') {
if (variant === 'outline') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_600,
})
} else {
baseStyles.push({
color: t.palette.contrast_300,
})
}
}
} else if (color === 'negative') {
if (variant === 'outline') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
}
} else if (color === 'negative_secondary') {
if (variant === 'outline') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
} else if (variant === 'ghost') {
if (!disabled) {
baseStyles.push({color: t.palette.negative_400})
} else {
baseStyles.push({color: t.palette.negative_400, opacity: 0.5})
}
}
} }
/*
* END DEPRECATED STYLES
*/
} }
if (size === 'large') { if (size === 'large') {
+6 -6
View File
@@ -378,8 +378,8 @@ export function ProfileGrid({
style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}> style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}>
<View <View
style={[ style={[
a.p_lg, a.px_lg,
a.py_md, a.pt_md,
a.flex_row, a.flex_row,
a.align_center, a.align_center,
a.justify_between, a.justify_between,
@@ -399,7 +399,7 @@ export function ProfileGrid({
</View> </View>
{gtMobile ? ( {gtMobile ? (
<View style={[a.px_lg, a.pb_lg]}> <View style={[a.p_lg, a.pt_md]}>
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}> <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
{content} {content}
</View> </View>
@@ -411,9 +411,8 @@ 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">
style={[a.overflow_visible]}> <View style={[a.p_lg, a.pt_md, a.flex_row, a.gap_md]}>
<View style={[a.px_lg, a.pb_lg, a.flex_row, a.gap_md]}>
{content} {content}
<SeeMoreSuggestedProfilesCard /> <SeeMoreSuggestedProfilesCard />
@@ -434,6 +433,7 @@ function SeeMoreSuggestedProfilesCard() {
return ( return (
<Button <Button
label={_(msg`Browse more accounts on the Explore page`)} label={_(msg`Browse more accounts on the Explore page`)}
style={[a.flex_col]}
onPress={() => { onPress={() => {
navigation.navigate('SearchTab') navigation.navigate('SearchTab')
}}> }}>
+4 -3
View File
@@ -1,7 +1,8 @@
import React, {memo} from 'react' import {memo} from 'react'
import {StyleProp, View, ViewStyle} from 'react-native' import {type StyleProp, View, type ViewStyle} 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 type React from 'react'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {CenteredView} from '#/view/com/util/Views' import {CenteredView} from '#/view/com/util/Views'
@@ -95,7 +96,7 @@ function ListFooterMaybeError({
)} )}
</Text> </Text>
<Button <Button
variant="gradient" variant="solid"
label={_(msg`Press to retry`)} label={_(msg`Press to retry`)}
style={[ style={[
a.align_center, a.align_center,
@@ -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 = ({
+205
View File
@@ -0,0 +1,205 @@
import {createContext, useContext, useMemo} from 'react'
import {View} from 'react-native'
import {atoms as a, select, useTheme} 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'
import {type ToastType} from '#/components/Toast/types'
import {Text} from '#/components/Typography'
type ContextType = {
type: ToastType
}
export const ICONS = {
default: SuccessIcon,
success: SuccessIcon,
error: ErrorIcon,
warning: WarningIcon,
info: CircleInfo,
}
const Context = createContext<ContextType>({
type: 'default',
})
export function Toast({
type,
content,
}: {
type: ToastType
content: React.ReactNode
}) {
const t = useTheme()
const styles = useToastStyles({type})
const Icon = ICONS[type]
return (
<Context.Provider value={useMemo(() => ({type}), [type])}>
<View
style={[
a.flex_1,
a.py_lg,
a.pl_xl,
a.pr_2xl,
a.rounded_md,
a.border,
a.flex_row,
a.gap_sm,
t.atoms.shadow_sm,
{
backgroundColor: styles.backgroundColor,
borderColor: styles.borderColor,
},
]}>
<Icon size="md" fill={styles.iconColor} />
<View style={[a.flex_1]}>
{typeof content === 'string' ? (
<ToastText>{content}</ToastText>
) : (
content
)}
</View>
</View>
</Context.Provider>
)
}
export function ToastText({children}: {children: React.ReactNode}) {
const {type} = useContext(Context)
const {textColor} = useToastStyles({type})
return (
<Text
style={[
a.text_md,
a.font_bold,
a.leading_snug,
{
color: textColor,
},
]}>
{children}
</Text>
)
}
function useToastStyles({type}: {type: ToastType}) {
const t = useTheme()
return useMemo(() => {
return {
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,
}),
},
}[type]
}, [t, type])
}
+1
View File
@@ -0,0 +1 @@
export const DEFAULT_TOAST_DURATION = 3000
+5
View File
@@ -0,0 +1,5 @@
export function ToastContainer() {
return null
}
export function show() {}
+197
View File
@@ -0,0 +1,197 @@
import {useEffect, useMemo, useRef, useState} from 'react'
import {AccessibilityInfo} from 'react-native'
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from 'react-native-gesture-handler'
import Animated, {
Easing,
runOnJS,
SlideInUp,
SlideOutUp,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withDecay,
withSpring,
} from 'react-native-reanimated'
import RootSiblings from 'react-native-root-siblings'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {atoms as a} from '#/alf'
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
import {Toast} from '#/components/Toast/Toast'
import {type ToastApi, type ToastType} from '#/components/Toast/types'
const TOAST_ANIMATION_DURATION = 300
export function ToastContainer() {
return null
}
export const toast: ToastApi = {
show(props) {
if (process.env.NODE_ENV === 'test') {
return
}
AccessibilityInfo.announceForAccessibility(props.a11yLabel)
const item = new RootSiblings(
(
<AnimatedToast
type={props.type}
content={props.content}
a11yLabel={props.a11yLabel}
duration={props.duration ?? DEFAULT_TOAST_DURATION}
destroy={() => item.destroy()}
/>
),
)
},
}
function AnimatedToast({
type,
content,
a11yLabel,
duration,
destroy,
}: {
type: ToastType
content: React.ReactNode
a11yLabel: string
duration: number
destroy: () => void
}) {
const {top} = useSafeAreaInsets()
const isPanning = useSharedValue(false)
const dismissSwipeTranslateY = useSharedValue(0)
const [cardHeight, setCardHeight] = useState(0)
// for the exit animation to work on iOS the animated component
// must not be the root component
// so we need to wrap it in a view and unmount the toast ahead of time
const [alive, setAlive] = useState(true)
const hideAndDestroyImmediately = () => {
setAlive(false)
setTimeout(() => {
destroy()
}, 1e3)
}
const destroyTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
const hideAndDestroyAfterTimeout = useNonReactiveCallback(() => {
clearTimeout(destroyTimeoutRef.current)
destroyTimeoutRef.current = setTimeout(hideAndDestroyImmediately, duration)
})
const pauseDestroy = useNonReactiveCallback(() => {
clearTimeout(destroyTimeoutRef.current)
})
useEffect(() => {
hideAndDestroyAfterTimeout()
}, [hideAndDestroyAfterTimeout])
const panGesture = useMemo(() => {
return Gesture.Pan()
.activeOffsetY([-10, 10])
.failOffsetX([-10, 10])
.maxPointers(1)
.onStart(() => {
'worklet'
if (!alive) return
isPanning.set(true)
runOnJS(pauseDestroy)()
})
.onUpdate(e => {
'worklet'
if (!alive) return
dismissSwipeTranslateY.value = e.translationY
})
.onEnd(e => {
'worklet'
if (!alive) return
runOnJS(hideAndDestroyAfterTimeout)()
isPanning.set(false)
if (e.velocityY < -100) {
if (dismissSwipeTranslateY.value === 0) {
// HACK: If the initial value is 0, withDecay() animation doesn't start.
// This is a bug in Reanimated, but for now we'll work around it like this.
dismissSwipeTranslateY.value = 1
}
dismissSwipeTranslateY.value = withDecay({
velocity: e.velocityY,
velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1),
deceleration: 1,
})
} else {
dismissSwipeTranslateY.value = withSpring(0, {
stiffness: 500,
damping: 50,
})
}
})
}, [
dismissSwipeTranslateY,
isPanning,
alive,
hideAndDestroyAfterTimeout,
pauseDestroy,
])
const topOffset = top + 10
useAnimatedReaction(
() =>
!isPanning.get() &&
dismissSwipeTranslateY.get() < -topOffset - cardHeight,
(isSwipedAway, prevIsSwipedAway) => {
'worklet'
if (isSwipedAway && !prevIsSwipedAway) {
runOnJS(destroy)()
}
},
)
const animatedStyle = useAnimatedStyle(() => {
const translation = dismissSwipeTranslateY.get()
return {
transform: [
{
translateY: translation > 0 ? translation ** 0.7 : translation,
},
],
}
})
return (
<GestureHandlerRootView
style={[a.absolute, {top: topOffset, left: 16, right: 16}]}
pointerEvents="box-none">
{alive && (
<Animated.View
entering={SlideInUp.easing(Easing.out(Easing.exp)).duration(
TOAST_ANIMATION_DURATION,
)}
exiting={SlideOutUp.easing(Easing.in(Easing.exp)).duration(
TOAST_ANIMATION_DURATION * 0.7,
)}
onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)}
accessibilityRole="alert"
accessible={true}
accessibilityLabel={a11yLabel}
accessibilityHint=""
onAccessibilityEscape={hideAndDestroyImmediately}
style={[a.flex_1, animatedStyle]}>
<GestureDetector gesture={panGesture}>
<Toast content={content} type={type} />
</GestureDetector>
</Animated.View>
)}
</GestureHandlerRootView>
)
}
+107
View File
@@ -0,0 +1,107 @@
/*
* Note: relies on styles in #/styles.css
*/
import {useEffect, useState} from 'react'
import {AccessibilityInfo, Pressable, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useBreakpoints} from '#/alf'
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
import {Toast} from '#/components/Toast/Toast'
import {type ToastApi, type ToastType} from '#/components/Toast/types'
const TOAST_ANIMATION_STYLES = {
entering: {
animation: 'toastFadeIn 0.3s ease-out forwards',
},
exiting: {
animation: 'toastFadeOut 0.2s ease-in forwards',
},
}
interface ActiveToast {
type: ToastType
content: React.ReactNode
a11yLabel: string
}
type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
let globalSetActiveToast: GlobalSetActiveToast | undefined
let toastTimeout: NodeJS.Timeout | undefined
type ToastContainerProps = {}
export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
const {_} = useLingui()
const {gtPhone} = useBreakpoints()
const [activeToast, setActiveToast] = useState<ActiveToast | undefined>()
const [isExiting, setIsExiting] = useState(false)
useEffect(() => {
globalSetActiveToast = (t: ActiveToast | undefined) => {
if (!t && activeToast) {
setIsExiting(true)
setTimeout(() => {
setActiveToast(t)
setIsExiting(false)
}, 200)
} else {
if (t) {
AccessibilityInfo.announceForAccessibility(t.a11yLabel)
}
setActiveToast(t)
setIsExiting(false)
}
}
}, [activeToast])
return (
<>
{activeToast && (
<View
style={[
a.fixed,
{
left: a.px_xl.paddingLeft,
right: a.px_xl.paddingLeft,
bottom: a.px_xl.paddingLeft,
...(isExiting
? TOAST_ANIMATION_STYLES.exiting
: TOAST_ANIMATION_STYLES.entering),
},
gtPhone && [
{
maxWidth: 380,
},
],
]}>
<Toast content={activeToast.content} type={activeToast.type} />
<Pressable
style={[a.absolute, a.inset_0]}
accessibilityLabel={_(msg`Dismiss toast`)}
accessibilityHint=""
onPress={() => setActiveToast(undefined)}
/>
</View>
)}
</>
)
}
export const toast: ToastApi = {
show(props) {
if (toastTimeout) {
clearTimeout(toastTimeout)
}
globalSetActiveToast?.({
type: props.type,
content: props.content,
a11yLabel: props.a11yLabel,
})
toastTimeout = setTimeout(() => {
globalSetActiveToast?.(undefined)
}, props.duration || DEFAULT_TOAST_DURATION)
},
}
+24
View File
@@ -0,0 +1,24 @@
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info'
export type ToastApi = {
show: (props: {
/**
* The type of toast to show. This determines the styling and icon used.
*/
type: ToastType
/**
* A string, `Text`, or `Span` components to render inside the toast. This
* allows additional formatting of the content, but should not be used for
* interactive elements link links or buttons.
*/
content: React.ReactNode | string
/**
* Accessibility label for the toast, used for screen readers.
*/
a11yLabel: string
/**
* Defaults to `DEFAULT_TOAST_DURATION` from `#components/Toast/const`.
*/
duration?: number
}) => void
}
-8
View File
@@ -1,8 +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'
export const GCP_PROJECT_ID = Number(process.env.EXPO_PUBLIC_GCP_PROJECT_ID)
+80
View File
@@ -0,0 +1,80 @@
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 === undefined
? 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'})`
+7 -3
View File
@@ -1,7 +1,8 @@
import React, {createContext, ReactNode, useContext} from 'react' import {type ReactNode} from 'react'
import {TextStyle, ViewStyle} from 'react-native' import {createContext, useContext} from 'react'
import {type TextStyle, type ViewStyle} from 'react-native'
import {ThemeName} from '#/alf/types' import {type ThemeName} from '#/alf/types'
import {darkTheme, defaultTheme, dimTheme} from './themes' import {darkTheme, defaultTheme, dimTheme} from './themes'
export type ColorScheme = 'light' | 'dark' export type ColorScheme = 'light' | 'dark'
@@ -29,6 +30,9 @@ export type Palette = Record<PaletteColorName, PaletteColor>
export type ShapeName = 'button' | 'bigButton' | 'smallButton' export type ShapeName = 'button' | 'bigButton' | 'smallButton'
export type Shapes = Record<ShapeName, ViewStyle> export type Shapes = Record<ShapeName, ViewStyle>
/**
* @deprecated use typography atoms from `#/alf`
*/
export type TypographyVariant = export type TypographyVariant =
| '2xl-thin' | '2xl-thin'
| '2xl' | '2xl'
-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
+10 -2
View File
@@ -1,7 +1,11 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {TextStyle, ViewStyle} from 'react-native' import {type TextStyle, type ViewStyle} from 'react-native'
import {PaletteColor, PaletteColorName, useTheme} from '../ThemeContext' import {
type PaletteColor,
type PaletteColorName,
useTheme,
} from '../ThemeContext'
export interface UsePaletteValue { export interface UsePaletteValue {
colors: PaletteColor colors: PaletteColor
@@ -16,6 +20,10 @@ export interface UsePaletteValue {
link: TextStyle link: TextStyle
icon: TextStyle icon: TextStyle
} }
/**
* @deprecated use `useTheme` from `#/alf`
*/
export function usePalette(color: PaletteColorName): UsePaletteValue { export function usePalette(color: PaletteColorName): UsePaletteValue {
const theme = useTheme() const theme = useTheme()
return useMemo(() => { return useMemo(() => {
+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,
}, },
+13 -2
View File
@@ -1,9 +1,17 @@
import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native' import {
Dimensions,
type StyleProp,
StyleSheet,
type TextStyle,
} from 'react-native'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {Theme, TypographyVariant} from './ThemeContext' import {type Theme, type TypographyVariant} from './ThemeContext'
// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest // 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
/**
* @deprecated use ALF colors instead
*/
export const colors = { export const colors = {
white: '#ffffff', white: '#ffffff',
black: '#000000', black: '#000000',
@@ -63,6 +71,9 @@ export const gradients = {
blueDark: {start: '#5F45E0', end: colors.blue3}, // avis, banner blueDark: {start: '#5F45E0', end: colors.blue3}, // avis, banner
} }
/**
* @deprecated use atoms from `#/alf`
*/
export const s = StyleSheet.create({ export const s = StyleSheet.create({
// helpers // helpers
footerSpacer: {height: 100}, footerSpacer: {height: 100},
+40 -42
View File
@@ -657,8 +657,8 @@ msgstr ""
#: src/view/com/composer/GifAltText.tsx:76 #: src/view/com/composer/GifAltText.tsx:76
#: src/view/com/composer/GifAltText.tsx:144 #: src/view/com/composer/GifAltText.tsx:144
#: src/view/com/composer/GifAltText.tsx:210 #: src/view/com/composer/GifAltText.tsx:210
#: src/view/com/composer/photos/Gallery.tsx:169 #: src/view/com/composer/photos/Gallery.tsx:170
#: src/view/com/composer/photos/Gallery.tsx:216 #: src/view/com/composer/photos/Gallery.tsx:217
#: src/view/com/composer/photos/ImageAltTextDialog.tsx:88 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:88
#: src/view/com/composer/photos/ImageAltTextDialog.tsx:93 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:93
msgid "Add alt text" msgid "Add alt text"
@@ -870,7 +870,7 @@ msgstr ""
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:186 #: src/components/Post/Embed/ExternalEmbed/Gif.tsx:186
#: src/view/com/composer/GifAltText.tsx:100 #: src/view/com/composer/GifAltText.tsx:100
#: src/view/com/composer/photos/Gallery.tsx:187 #: src/view/com/composer/photos/Gallery.tsx:188
msgid "ALT" msgid "ALT"
msgstr "" msgstr ""
@@ -888,7 +888,7 @@ msgstr ""
msgid "Alt Text" msgid "Alt Text"
msgstr "" msgstr ""
#: src/view/com/composer/photos/Gallery.tsx:255 #: src/view/com/composer/photos/Gallery.tsx:260
msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone." msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone."
msgstr "" msgstr ""
@@ -1239,7 +1239,7 @@ msgstr ""
#: src/components/dms/dialogs/NewChatDialog.tsx:54 #: src/components/dms/dialogs/NewChatDialog.tsx:54
#: src/components/dms/MessageProfileButton.tsx:58 #: src/components/dms/MessageProfileButton.tsx:58
#: src/screens/Messages/ChatList.tsx:362 #: src/screens/Messages/ChatList.tsx:358
#: src/screens/Messages/Conversation.tsx:228 #: src/screens/Messages/Conversation.tsx:228
msgid "Before you can message another user, you must first verify your email." msgid "Before you can message another user, you must first verify your email."
msgstr "" msgstr ""
@@ -1423,7 +1423,7 @@ msgstr ""
msgid "Books" msgid "Books"
msgstr "" msgstr ""
#: src/components/FeedInterstitials.tsx:436 #: src/components/FeedInterstitials.tsx:435
msgid "Browse more accounts on the Explore page" msgid "Browse more accounts on the Explore page"
msgstr "" msgstr ""
@@ -1663,11 +1663,11 @@ msgid "Chat muted"
msgstr "" msgstr ""
#: src/Navigation.tsx:558 #: src/Navigation.tsx:558
#: src/screens/Messages/components/InboxPreview.tsx:24 #: src/screens/Messages/components/InboxPreview.tsx:22
msgid "Chat request inbox" msgid "Chat request inbox"
msgstr "" msgstr ""
#: src/screens/Messages/components/InboxPreview.tsx:64 #: src/screens/Messages/components/InboxPreview.tsx:62
#: src/screens/Messages/Inbox.tsx:56 #: src/screens/Messages/Inbox.tsx:56
#: src/screens/Messages/Inbox.tsx:98 #: src/screens/Messages/Inbox.tsx:98
msgid "Chat requests" msgid "Chat requests"
@@ -1675,7 +1675,7 @@ msgstr ""
#: src/components/dms/ConvoMenu.tsx:75 #: src/components/dms/ConvoMenu.tsx:75
#: src/Navigation.tsx:553 #: src/Navigation.tsx:553
#: src/screens/Messages/ChatList.tsx:371 #: src/screens/Messages/ChatList.tsx:367
msgid "Chat settings" msgid "Chat settings"
msgstr "" msgstr ""
@@ -1690,8 +1690,8 @@ msgid "Chat unmuted"
msgstr "" msgstr ""
#: src/screens/Messages/ChatList.tsx:76 #: src/screens/Messages/ChatList.tsx:76
#: src/screens/Messages/ChatList.tsx:387 #: src/screens/Messages/ChatList.tsx:383
#: src/screens/Messages/ChatList.tsx:411 #: src/screens/Messages/ChatList.tsx:407
msgid "Chats" msgid "Chats"
msgstr "" msgstr ""
@@ -2309,12 +2309,9 @@ msgstr ""
msgid "Create Account" msgid "Create Account"
msgstr "" msgstr ""
#: src/screens/Search/SearchResults.tsx:266
msgid "create an account"
msgstr ""
#: src/components/dialogs/Signin.tsx:86 #: src/components/dialogs/Signin.tsx:86
#: src/components/dialogs/Signin.tsx:88 #: src/components/dialogs/Signin.tsx:88
#: src/screens/Search/SearchResults.tsx:266
msgid "Create an account" msgid "Create an account"
msgstr "" msgstr ""
@@ -2666,6 +2663,10 @@ msgstr ""
msgid "Dismiss this section" msgid "Dismiss this section"
msgstr "" msgstr ""
#: src/components/Toast/index.web.tsx:81
msgid "Dismiss toast"
msgstr ""
#: src/screens/Settings/AccessibilitySettings.tsx:69 #: src/screens/Settings/AccessibilitySettings.tsx:69
#: src/screens/Settings/AccessibilitySettings.tsx:74 #: src/screens/Settings/AccessibilitySettings.tsx:74
msgid "Display larger alt text badges" msgid "Display larger alt text badges"
@@ -2846,7 +2847,7 @@ msgstr ""
#: src/view/com/composer/photos/EditImageDialog.web.tsx:85 #: src/view/com/composer/photos/EditImageDialog.web.tsx:85
#: src/view/com/composer/photos/EditImageDialog.web.tsx:89 #: src/view/com/composer/photos/EditImageDialog.web.tsx:89
#: src/view/com/composer/photos/Gallery.tsx:194 #: src/view/com/composer/photos/Gallery.tsx:195
msgid "Edit image" msgid "Edit image"
msgstr "" msgstr ""
@@ -3339,7 +3340,7 @@ msgstr ""
msgid "Failed to delete starter pack" msgid "Failed to delete starter pack"
msgstr "" msgstr ""
#: src/screens/Messages/ChatList.tsx:274 #: src/screens/Messages/ChatList.tsx:270
#: src/screens/Messages/Inbox.tsx:208 #: src/screens/Messages/Inbox.tsx:208
msgid "Failed to load conversations" msgid "Failed to load conversations"
msgstr "" msgstr ""
@@ -5300,8 +5301,8 @@ msgid "New {postsCount, plural, one {post} other {posts}} from {firstAuthorName}
msgstr "" msgstr ""
#: src/components/dms/dialogs/NewChatDialog.tsx:67 #: src/components/dms/dialogs/NewChatDialog.tsx:67
#: src/screens/Messages/ChatList.tsx:394 #: src/screens/Messages/ChatList.tsx:390
#: src/screens/Messages/ChatList.tsx:401 #: src/screens/Messages/ChatList.tsx:397
msgid "New chat" msgid "New chat"
msgstr "" msgstr ""
@@ -5515,7 +5516,7 @@ msgstr ""
msgid "No results for \"{0}\"." msgid "No results for \"{0}\"."
msgstr "" msgstr ""
#: src/components/Lists.tsx:189 #: src/components/Lists.tsx:190
msgid "No results found" msgid "No results found"
msgstr "" msgstr ""
@@ -5592,7 +5593,7 @@ msgstr ""
msgid "Note: This post is only visible to logged-in users." msgid "Note: This post is only visible to logged-in users."
msgstr "" msgstr ""
#: src/screens/Messages/ChatList.tsx:295 #: src/screens/Messages/ChatList.tsx:291
msgid "Nothing here" msgid "Nothing here"
msgstr "" msgstr ""
@@ -5740,11 +5741,11 @@ msgstr ""
msgid "Only WebVTT (.vtt) files are supported" msgid "Only WebVTT (.vtt) files are supported"
msgstr "" msgstr ""
#: src/components/Lists.tsx:94 #: src/components/Lists.tsx:95
msgid "Oops, something went wrong!" msgid "Oops, something went wrong!"
msgstr "" msgstr ""
#: src/components/Lists.tsx:173 #: src/components/Lists.tsx:174
#: src/components/StarterPack/ProfileStarterPacks.tsx:332 #: src/components/StarterPack/ProfileStarterPacks.tsx:332
#: src/components/StarterPack/ProfileStarterPacks.tsx:341 #: src/components/StarterPack/ProfileStarterPacks.tsx:341
#: src/screens/Settings/AppPasswords.tsx:59 #: src/screens/Settings/AppPasswords.tsx:59
@@ -5834,7 +5835,7 @@ msgstr ""
msgid "Open system log" msgid "Open system log"
msgstr "" msgstr ""
#: src/view/com/util/forms/DropdownButton.tsx:162 #: src/view/com/util/forms/DropdownButton.tsx:167
msgid "Opens {numItems} options" msgid "Opens {numItems} options"
msgstr "" msgstr ""
@@ -5980,7 +5981,7 @@ msgstr ""
msgid "Our moderators have reviewed reports and decided to disable your access to chats on Bluesky." msgid "Our moderators have reviewed reports and decided to disable your access to chats on Bluesky."
msgstr "" msgstr ""
#: src/components/Lists.tsx:190 #: src/components/Lists.tsx:191
#: src/view/screens/NotFound.tsx:47 #: src/view/screens/NotFound.tsx:47
msgid "Page not found" msgid "Page not found"
msgstr "" msgstr ""
@@ -6408,7 +6409,7 @@ msgid "Press to attempt reconnection"
msgstr "" msgstr ""
#: src/components/Error.tsx:60 #: src/components/Error.tsx:60
#: src/components/Lists.tsx:99 #: src/components/Lists.tsx:100
#: src/screens/Messages/components/MessageListError.tsx:24 #: src/screens/Messages/components/MessageListError.tsx:24
#: src/screens/Signup/BackNextButtons.tsx:47 #: src/screens/Signup/BackNextButtons.tsx:47
msgid "Press to retry" msgid "Press to retry"
@@ -6688,7 +6689,7 @@ msgstr ""
msgid "Reject chat request" msgid "Reject chat request"
msgstr "" msgstr ""
#: src/screens/Messages/ChatList.tsx:278 #: src/screens/Messages/ChatList.tsx:274
#: src/screens/Messages/Inbox.tsx:212 #: src/screens/Messages/Inbox.tsx:212
msgid "Reload conversations" msgid "Reload conversations"
msgstr "" msgstr ""
@@ -6765,7 +6766,7 @@ msgstr ""
msgid "Remove from your feeds?" msgid "Remove from your feeds?"
msgstr "" msgstr ""
#: src/view/com/composer/photos/Gallery.tsx:203 #: src/view/com/composer/photos/Gallery.tsx:204
msgid "Remove image" msgid "Remove image"
msgstr "" msgstr ""
@@ -7151,14 +7152,14 @@ msgstr ""
#: src/components/dms/MessageItem.tsx:321 #: src/components/dms/MessageItem.tsx:321
#: src/components/Error.tsx:65 #: src/components/Error.tsx:65
#: src/components/Lists.tsx:110 #: src/components/Lists.tsx:111
#: src/components/moderation/ReportDialog/index.tsx:229 #: src/components/moderation/ReportDialog/index.tsx:229
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:55 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:55
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:57 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:57
#: src/components/StarterPack/ProfileStarterPacks.tsx:346 #: src/components/StarterPack/ProfileStarterPacks.tsx:346
#: src/screens/Login/LoginForm.tsx:323 #: src/screens/Login/LoginForm.tsx:323
#: src/screens/Login/LoginForm.tsx:330 #: src/screens/Login/LoginForm.tsx:330
#: src/screens/Messages/ChatList.tsx:284 #: src/screens/Messages/ChatList.tsx:280
#: src/screens/Messages/components/MessageListError.tsx:25 #: src/screens/Messages/components/MessageListError.tsx:25
#: src/screens/Messages/Inbox.tsx:218 #: src/screens/Messages/Inbox.tsx:218
#: src/screens/Onboarding/StepInterests/index.tsx:217 #: src/screens/Onboarding/StepInterests/index.tsx:217
@@ -7538,7 +7539,7 @@ msgstr ""
msgid "Select your preferred notification channels" msgid "Select your preferred notification channels"
msgstr "" msgstr ""
#: src/view/com/util/forms/DropdownButton.tsx:297 #: src/view/com/util/forms/DropdownButton.tsx:302
msgid "Selects option {0} of {numItems}" msgid "Selects option {0} of {numItems}"
msgstr "" msgstr ""
@@ -7901,15 +7902,12 @@ msgstr ""
msgid "Shows the content" msgid "Shows the content"
msgstr "" msgstr ""
#: src/screens/Search/SearchResults.tsx:258
msgid "sign in"
msgstr ""
#: src/components/dialogs/Signin.tsx:97 #: src/components/dialogs/Signin.tsx:97
#: src/components/dialogs/Signin.tsx:99 #: src/components/dialogs/Signin.tsx:99
#: src/screens/Login/index.tsx:122 #: src/screens/Login/index.tsx:122
#: src/screens/Login/index.tsx:143 #: src/screens/Login/index.tsx:143
#: src/screens/Login/LoginForm.tsx:181 #: src/screens/Login/LoginForm.tsx:181
#: src/screens/Search/SearchResults.tsx:258
#: src/view/com/auth/SplashScreen.tsx:61 #: src/view/com/auth/SplashScreen.tsx:61
#: src/view/com/auth/SplashScreen.tsx:69 #: src/view/com/auth/SplashScreen.tsx:69
#: src/view/com/auth/SplashScreen.web.tsx:123 #: src/view/com/auth/SplashScreen.web.tsx:123
@@ -8051,7 +8049,7 @@ msgstr ""
msgid "Something went wrong, please try again." msgid "Something went wrong, please try again."
msgstr "" msgstr ""
#: src/components/Lists.tsx:174 #: src/components/Lists.tsx:175
msgid "Something went wrong!" msgid "Something went wrong!"
msgstr "" msgstr ""
@@ -8933,7 +8931,7 @@ msgstr ""
msgid "Today" msgid "Today"
msgstr "" msgstr ""
#: src/view/com/util/forms/DropdownButton.tsx:258 #: src/view/com/util/forms/DropdownButton.tsx:263
msgid "Toggle dropdown" msgid "Toggle dropdown"
msgstr "" msgstr ""
@@ -9517,7 +9515,7 @@ msgstr ""
#: src/screens/Settings/AboutSettings.tsx:126 #: src/screens/Settings/AboutSettings.tsx:126
#: src/screens/Settings/AboutSettings.tsx:155 #: src/screens/Settings/AboutSettings.tsx:155
msgid "Version {appVersion}" msgid "Version {0}"
msgstr "" msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:83 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:83
@@ -9841,7 +9839,7 @@ msgstr ""
msgid "We're sorry! The post you are replying to has been deleted." msgid "We're sorry! The post you are replying to has been deleted."
msgstr "" msgstr ""
#: src/components/Lists.tsx:194 #: src/components/Lists.tsx:195
#: src/view/screens/NotFound.tsx:50 #: src/view/screens/NotFound.tsx:50
msgid "We're sorry! We can't find the page you were looking for." msgid "We're sorry! We can't find the page you were looking for."
msgstr "" msgstr ""
@@ -9901,7 +9899,7 @@ msgid "Who can verify?"
msgstr "" msgstr ""
#: src/screens/Home/NoFeedsPinned.tsx:79 #: src/screens/Home/NoFeedsPinned.tsx:79
#: src/screens/Messages/ChatList.tsx:262 #: src/screens/Messages/ChatList.tsx:258
#: src/screens/Messages/Inbox.tsx:197 #: src/screens/Messages/Inbox.tsx:197
msgid "Whoops!" msgid "Whoops!"
msgstr "" msgstr ""
@@ -10173,7 +10171,7 @@ msgstr ""
msgid "You have muted this user" msgid "You have muted this user"
msgstr "" msgstr ""
#: src/screens/Messages/ChatList.tsx:305 #: src/screens/Messages/ChatList.tsx:301
msgid "You have no conversations yet. Start one!" msgid "You have no conversations yet. Start one!"
msgstr "" msgstr ""
@@ -10194,7 +10192,7 @@ msgstr ""
msgid "You have not muted any accounts yet. To mute an account, go to their profile and select \"Mute account\" from the menu on their account." msgid "You have not muted any accounts yet. To mute an account, go to their profile and select \"Mute account\" from the menu on their account."
msgstr "" msgstr ""
#: src/components/Lists.tsx:57 #: src/components/Lists.tsx:58
msgid "You have reached the end" msgid "You have reached the end"
msgstr "" msgstr ""
+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
+26 -30
View File
@@ -55,7 +55,7 @@ type ListItem =
function renderItem({item}: {item: ListItem}) { function renderItem({item}: {item: ListItem}) {
switch (item.type) { switch (item.type) {
case 'INBOX': case 'INBOX':
return <InboxPreview count={item.count} profiles={item.profiles} /> return <InboxPreview profiles={item.profiles} />
case 'CONVERSATION': case 'CONVERSATION':
return <ChatListItem convo={item.conversation} /> return <ChatListItem convo={item.conversation} />
} }
@@ -140,22 +140,24 @@ export function MessagesScreenInner({navigation, route}: Props) {
const leftConvos = useLeftConvos() const leftConvos = useLeftConvos()
const inboxPreviewConvos = useMemo(() => { const inboxAllConvos =
const inbox = inboxData?.pages
inboxData?.pages .flatMap(page => page.convos)
.flatMap(page => page.convos) .filter(
.filter( convo =>
convo => !leftConvos.includes(convo.id) &&
!leftConvos.includes(convo.id) && !convo.muted &&
!convo.muted && convo.members.every(member => member.handle !== 'missing.invalid'),
convo.unreadCount > 0 && ) ?? []
convo.members.every(member => member.handle !== 'missing.invalid'), const hasInboxConvos = inboxAllConvos?.length > 0
) ?? []
return inbox const inboxUnreadConvos = inboxAllConvos.filter(
.map(x => x.members.find(y => y.did !== currentAccount?.did)) convo => convo.unreadCount > 0,
.filter(x => !!x) )
}, [inboxData, leftConvos, currentAccount?.did])
const inboxUnreadConvoMembers = inboxUnreadConvos
.map(x => x.members.find(y => y.did !== currentAccount?.did))
.filter(x => !!x)
const conversations = useMemo(() => { const conversations = useMemo(() => {
if (data?.pages) { if (data?.pages) {
@@ -164,15 +166,13 @@ export function MessagesScreenInner({navigation, route}: Props) {
// filter out convos that are actively being left // filter out convos that are actively being left
.filter(convo => !leftConvos.includes(convo.id)) .filter(convo => !leftConvos.includes(convo.id))
const hasInboxRequests = inboxPreviewConvos?.length > 0
return [ return [
...(hasInboxRequests ...(hasInboxConvos
? [ ? [
{ {
type: 'INBOX' as const, type: 'INBOX' as const,
count: inboxPreviewConvos.length, count: inboxUnreadConvoMembers.length,
profiles: inboxPreviewConvos.slice(0, 3), profiles: inboxUnreadConvoMembers.slice(0, 3),
}, },
] ]
: []), : []),
@@ -182,7 +182,7 @@ export function MessagesScreenInner({navigation, route}: Props) {
] satisfies ListItem[] ] satisfies ListItem[]
} }
return [] return []
}, [data, leftConvos, inboxPreviewConvos]) }, [data, leftConvos, hasInboxConvos, inboxUnreadConvoMembers])
const onRefresh = useCallback(async () => { const onRefresh = useCallback(async () => {
setIsPTRing(true) setIsPTRing(true)
@@ -231,21 +231,17 @@ export function MessagesScreenInner({navigation, route}: Props) {
// NOTE(APiligrim) // NOTE(APiligrim)
// Show empty state only if there are no conversations at all // Show empty state only if there are no conversations at all
const actualConversations = conversations.filter( const activeConversations = conversations.filter(
item => item.type === 'CONVERSATION', item => item.type === 'CONVERSATION',
) )
const hasInboxRequests = inboxPreviewConvos?.length > 0
if (actualConversations.length === 0) { if (activeConversations.length === 0) {
return ( return (
<Layout.Screen> <Layout.Screen>
<Header newChatControl={newChatControl} /> <Header newChatControl={newChatControl} />
<Layout.Center> <Layout.Center>
{hasInboxRequests && ( {!isLoading && hasInboxConvos && (
<InboxPreview <InboxPreview profiles={inboxUnreadConvoMembers} />
count={inboxPreviewConvos.length}
profiles={inboxPreviewConvos}
/>
)} )}
{isLoading ? ( {isLoading ? (
<ChatListLoadingPlaceholder /> <ChatListLoadingPlaceholder />
@@ -1,5 +1,5 @@
import {View} from 'react-native' import {View} from 'react-native'
import {ChatBskyActorDefs} from '@atproto/api' import {type ChatBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -12,10 +12,8 @@ import {Link} from '#/components/Link'
export function InboxPreview({ export function InboxPreview({
profiles, profiles,
}: // count, }: {
{
profiles: ChatBskyActorDefs.ProfileViewBasic[] profiles: ChatBskyActorDefs.ProfileViewBasic[]
count: number
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
+2 -2
View File
@@ -305,8 +305,8 @@ export function StepFinished() {
<Button <Button
disabled={saving} disabled={saving}
key={state.activeStep} // remove focus state on nav key={state.activeStep} // remove focus state on nav
variant="gradient" variant="solid"
color="gradient_sky" color="primary"
size="large" size="large"
label={_(msg`Complete onboarding and start using your account`)} label={_(msg`Complete onboarding and start using your account`)}
onPress={finishOnboarding}> onPress={finishOnboarding}>
@@ -15,7 +15,7 @@ import {
TitleText, TitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import { import {
ApiResponseMap, type ApiResponseMap,
Context, Context,
useInterestsDisplayNames, useInterestsDisplayNames,
} from '#/screens/Onboarding/state' } from '#/screens/Onboarding/state'
@@ -235,8 +235,8 @@ export function StepInterests() {
) : ( ) : (
<Button <Button
disabled={saving || !data} disabled={saving || !data}
variant="gradient" variant="solid"
color="gradient_sky" color="primary"
size="large" size="large"
label={_(msg`Continue to next step`)} label={_(msg`Continue to next step`)}
onPress={saveInterests}> onPress={saveInterests}>
+2 -2
View File
@@ -267,8 +267,8 @@ export function StepProfile() {
<OnboardingControls.Portal> <OnboardingControls.Portal>
<View style={[a.gap_md, gtMobile && {flexDirection: 'row-reverse'}]}> <View style={[a.gap_md, gtMobile && {flexDirection: 'row-reverse'}]}>
<Button <Button
variant="gradient" variant="solid"
color="gradient_sky" color="primary"
size="large" size="large"
label={_(msg`Continue to next step`)} label={_(msg`Continue to next step`)}
onPress={onContinue}> onPress={onContinue}>
+2 -2
View File
@@ -255,7 +255,7 @@ let SearchScreenPostResults = ({
<Trans> <Trans>
<InlineLinkText <InlineLinkText
style={[pal.link]} style={[pal.link]}
label={_(msg`sign in`)} label={_(msg`Sign in`)}
to={'#'} to={'#'}
onPress={showSignIn}> onPress={showSignIn}>
Sign in Sign in
@@ -263,7 +263,7 @@ let SearchScreenPostResults = ({
<Text style={t.atoms.text_contrast_medium}> or </Text> <Text style={t.atoms.text_contrast_medium}> or </Text>
<InlineLinkText <InlineLinkText
style={[pal.link]} style={[pal.link]}
label={_(msg`create an account`)} label={_(msg`Create an account`)}
to={'#'} to={'#'}
onPress={showCreateAccount}> onPress={showCreateAccount}>
create an account create an account
+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'>
@@ -124,7 +124,7 @@ export function LinkItem({
contentContainerStyle, contentContainerStyle,
chevronColor, chevronColor,
...props ...props
}: LinkProps & { }: Omit<LinkProps, Button.UninheritableButtonProps> & {
contentContainerStyle?: StyleProp<ViewStyle> contentContainerStyle?: StyleProp<ViewStyle>
destructive?: boolean destructive?: boolean
chevronColor?: string chevronColor?: string
@@ -132,7 +132,7 @@ export function LinkItem({
const t = useTheme() const t = useTheme()
return ( return (
<Link color="secondary" {...props}> <Link {...props}>
{args => ( {args => (
<Item <Item
destructive={destructive} destructive={destructive}
@@ -154,7 +154,7 @@ export function PressableItem({
contentContainerStyle, contentContainerStyle,
hoverStyle, hoverStyle,
...props ...props
}: Button.ButtonProps & { }: Omit<Button.ButtonProps, Button.UninheritableButtonProps> & {
contentContainerStyle?: StyleProp<ViewStyle> contentContainerStyle?: StyleProp<ViewStyle>
destructive?: boolean destructive?: boolean
}) { }) {
+9
View File
@@ -43,6 +43,9 @@ export interface ChangePasswordModal {
name: 'change-password' name: 'change-password'
} }
/**
* @deprecated DO NOT ADD NEW MODALS
*/
export type Modal = export type Modal =
// Account // Account
| DeleteAccountModal | DeleteAccountModal
@@ -125,10 +128,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
) )
} }
/**
* @deprecated use the dialog system from `#/components/Dialog.tsx`
*/
export function useModals() { export function useModals() {
return React.useContext(ModalContext) return React.useContext(ModalContext)
} }
/**
* @deprecated use the dialog system from `#/components/Dialog.tsx`
*/
export function useModalControls() { export function useModalControls() {
return React.useContext(ModalControlContext) return React.useContext(ModalControlContext)
} }
+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
+20
View File
@@ -369,3 +369,23 @@ input[type='range'][orient='vertical']::-moz-range-thumb {
transform: translateY(0); transform: translateY(0);
} }
} }
/*
* #/components/Toast/index.web.tsx
*/
@keyframes toastFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes toastFadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
+5
View File
@@ -21,6 +21,7 @@ import {type ComposerImage, cropImage} from '#/state/gallery'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {type PostAction} from '../state/composer' import {type PostAction} from '../state/composer'
import {EditImageDialog} from './EditImageDialog' import {EditImageDialog} from './EditImageDialog'
import {ImageAltTextDialog} from './ImageAltTextDialog' import {ImageAltTextDialog} from './ImageAltTextDialog'
@@ -227,8 +228,12 @@ const GalleryItem = ({
}} }}
accessible={true} accessible={true}
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
cachePolicy="none"
autoplay={false}
/> />
<MediaInsetBorder />
<ImageAltTextDialog <ImageAltTextDialog
control={altTextControl} control={altTextControl}
image={image} image={image}
+9
View File
@@ -56,6 +56,9 @@ interface Props extends React.ComponentProps<typeof TouchableOpacity> {
onBeforePress?: () => void onBeforePress?: () => void
} }
/**
* @deprecated use Link from `#/components/Link.tsx` instead
*/
export const Link = memo(function Link({ export const Link = memo(function Link({
testID, testID,
style, style,
@@ -156,6 +159,9 @@ export const Link = memo(function Link({
) )
}) })
/**
* @deprecated use InlineLinkText from `#/components/Link.tsx` instead
*/
export const TextLink = memo(function TextLink({ export const TextLink = memo(function TextLink({
testID, testID,
type = 'md', type = 'md',
@@ -301,6 +307,9 @@ interface TextLinkOnWebOnlyProps extends TextProps {
onPointerEnter?: () => void onPointerEnter?: () => void
anchorNoUnderline?: boolean anchorNoUnderline?: boolean
} }
/**
* @deprecated use WebOnlyInlineLinkText from `#/components/Link.tsx` instead
*/
export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
testID, testID,
type = 'md', type = 'md',
-1
View File
@@ -1 +0,0 @@
export function show() {}
-201
View File
@@ -1,201 +0,0 @@
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;
}
}
`
+46 -226
View File
@@ -1,234 +1,54 @@
import {useEffect, useMemo, useRef, useState} from 'react' import {toast} from '#/components/Toast'
import {AccessibilityInfo, View} from 'react-native' import {type ToastType} from '#/components/Toast/types'
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from 'react-native-gesture-handler'
import Animated, {
FadeIn,
FadeOut,
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withDecay,
withSpring,
} from 'react-native-reanimated'
import RootSiblings from 'react-native-root-siblings'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {
convertLegacyToastType,
getToastTypeStyles,
type LegacyToastType,
TOAST_ANIMATION_CONFIG,
TOAST_TYPE_TO_ICON,
type ToastType,
} from '#/view/com/util/Toast.style'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
const TIMEOUT = 2e3
// Use type overloading to mark certain types as deprecated -sfn
// https://stackoverflow.com/a/78325851/13325987
export function show(message: string, type?: ToastType): void
/** /**
* @deprecated type is deprecated - use one of `'default' | 'success' | 'error' | 'warning' | 'info'` * @deprecated use {@link ToastType} and {@link toast} instead
*/
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'
}
}
/**
* @deprecated use {@link toast} instead
*/ */
export function show(message: string, type?: LegacyToastType): void
export function show( export function show(
message: string, message: string,
type: ToastType | LegacyToastType = 'default', type: ToastType | LegacyToastType = 'default',
): void { ): void {
if (process.env.NODE_ENV === 'test') { const convertedType = convertLegacyToastType(type)
return toast.show({
} type: convertedType,
content: message,
AccessibilityInfo.announceForAccessibility(message) a11yLabel: message,
const item = new RootSiblings( })
(
<Toast
message={message}
type={convertLegacyToastType(type)}
destroy={() => item.destroy()}
/>
),
)
}
function Toast({
message,
type,
destroy,
}: {
message: string
type: ToastType
destroy: () => void
}) {
const t = useTheme()
const {top} = useSafeAreaInsets()
const isPanning = useSharedValue(false)
const dismissSwipeTranslateY = useSharedValue(0)
const [cardHeight, setCardHeight] = useState(0)
const toastStyles = getToastTypeStyles(t)
const colors = toastStyles[type]
const IconComponent = TOAST_TYPE_TO_ICON[type]
// for the exit animation to work on iOS the animated component
// must not be the root component
// so we need to wrap it in a view and unmount the toast ahead of time
const [alive, setAlive] = useState(true)
const hideAndDestroyImmediately = () => {
setAlive(false)
setTimeout(() => {
destroy()
}, 1e3)
}
const destroyTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
const hideAndDestroyAfterTimeout = useNonReactiveCallback(() => {
clearTimeout(destroyTimeoutRef.current)
destroyTimeoutRef.current = setTimeout(hideAndDestroyImmediately, TIMEOUT)
})
const pauseDestroy = useNonReactiveCallback(() => {
clearTimeout(destroyTimeoutRef.current)
})
useEffect(() => {
hideAndDestroyAfterTimeout()
}, [hideAndDestroyAfterTimeout])
const panGesture = useMemo(() => {
return Gesture.Pan()
.activeOffsetY([-10, 10])
.failOffsetX([-10, 10])
.maxPointers(1)
.onStart(() => {
'worklet'
if (!alive) return
isPanning.set(true)
runOnJS(pauseDestroy)()
})
.onUpdate(e => {
'worklet'
if (!alive) return
dismissSwipeTranslateY.value = e.translationY
})
.onEnd(e => {
'worklet'
if (!alive) return
runOnJS(hideAndDestroyAfterTimeout)()
isPanning.set(false)
if (e.velocityY < -100) {
if (dismissSwipeTranslateY.value === 0) {
// HACK: If the initial value is 0, withDecay() animation doesn't start.
// This is a bug in Reanimated, but for now we'll work around it like this.
dismissSwipeTranslateY.value = 1
}
dismissSwipeTranslateY.value = withDecay({
velocity: e.velocityY,
velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1),
deceleration: 1,
})
} else {
dismissSwipeTranslateY.value = withSpring(0, {
stiffness: 500,
damping: 50,
})
}
})
}, [
dismissSwipeTranslateY,
isPanning,
alive,
hideAndDestroyAfterTimeout,
pauseDestroy,
])
const topOffset = top + 10
useAnimatedReaction(
() =>
!isPanning.get() &&
dismissSwipeTranslateY.get() < -topOffset - cardHeight,
(isSwipedAway, prevIsSwipedAway) => {
'worklet'
if (isSwipedAway && !prevIsSwipedAway) {
runOnJS(destroy)()
}
},
)
const animatedStyle = useAnimatedStyle(() => {
const translation = dismissSwipeTranslateY.get()
return {
transform: [
{
translateY: translation > 0 ? translation ** 0.7 : translation,
},
],
}
})
return (
<GestureHandlerRootView
style={[a.absolute, {top: topOffset, left: 16, right: 16}]}
pointerEvents="box-none">
{alive && (
<Animated.View
entering={FadeIn.duration(TOAST_ANIMATION_CONFIG.duration)}
exiting={FadeOut.duration(TOAST_ANIMATION_CONFIG.duration * 0.7)}
onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)}
accessibilityRole="alert"
accessible={true}
accessibilityLabel={message}
accessibilityHint=""
onAccessibilityEscape={hideAndDestroyImmediately}
style={[
a.flex_1,
{backgroundColor: colors.backgroundColor},
a.shadow_sm,
{borderColor: colors.borderColor, borderWidth: 1},
a.rounded_sm,
animatedStyle,
]}>
<GestureDetector gesture={panGesture}>
<View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}>
<View
style={[
a.flex_shrink_0,
a.rounded_full,
{width: 32, height: 32},
a.align_center,
a.justify_center,
{
backgroundColor: colors.backgroundColor,
},
]}>
<IconComponent fill={colors.iconColor} size="sm" />
</View>
<View
style={[
a.h_full,
a.justify_center,
a.flex_1,
a.justify_center,
]}>
<Text
style={[a.text_md, a.font_bold, {color: colors.textColor}]}
emoji>
{message}
</Text>
</View>
</View>
</GestureDetector>
</Animated.View>
)}
</GestureHandlerRootView>
)
} }
-180
View File
@@ -1,180 +0,0 @@
/*
* Note: the dataSet properties are used to leverage custom CSS in public/index.html
*/
import {useEffect, useState} from 'react'
import {Pressable, StyleSheet, Text, View} from 'react-native'
import {
convertLegacyToastType,
getToastTypeStyles,
getToastWebAnimationStyles,
type LegacyToastType,
TOAST_TYPE_TO_ICON,
TOAST_WEB_KEYFRAMES,
type ToastType,
} from '#/view/com/util/Toast.style'
import {atoms as a, useTheme} from '#/alf'
const DURATION = 3500
interface ActiveToast {
text: string
type: ToastType
}
type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
// globals
// =
let globalSetActiveToast: GlobalSetActiveToast | undefined
let toastTimeout: NodeJS.Timeout | undefined
// components
// =
type ToastContainerProps = {}
export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
const [activeToast, setActiveToast] = useState<ActiveToast | undefined>()
const [isExiting, setIsExiting] = useState(false)
useEffect(() => {
globalSetActiveToast = (t: ActiveToast | undefined) => {
if (!t && activeToast) {
setIsExiting(true)
setTimeout(() => {
setActiveToast(t)
setIsExiting(false)
}, 200)
} else {
setActiveToast(t)
setIsExiting(false)
}
}
}, [activeToast])
useEffect(() => {
const styleId = 'toast-animations'
if (!document.getElementById(styleId)) {
const style = document.createElement('style')
style.id = styleId
style.textContent = TOAST_WEB_KEYFRAMES
document.head.appendChild(style)
}
}, [])
const t = useTheme()
const toastTypeStyles = getToastTypeStyles(t)
const toastStyles = activeToast
? toastTypeStyles[activeToast.type]
: toastTypeStyles.default
const IconComponent = activeToast
? TOAST_TYPE_TO_ICON[activeToast.type]
: TOAST_TYPE_TO_ICON.default
const animationStyles = getToastWebAnimationStyles()
return (
<>
{activeToast && (
<View
style={[
styles.container,
{
backgroundColor: toastStyles.backgroundColor,
borderColor: toastStyles.borderColor,
...(isExiting
? animationStyles.exiting
: animationStyles.entering),
},
]}>
<View
style={[
styles.iconContainer,
{
backgroundColor: 'transparent',
},
]}>
<IconComponent
fill={toastStyles.iconColor}
size="sm"
style={styles.icon}
/>
</View>
<Text
style={[
styles.text,
a.text_sm,
a.font_bold,
{color: toastStyles.textColor},
]}>
{activeToast.text}
</Text>
<Pressable
style={styles.dismissBackdrop}
accessibilityLabel="Dismiss"
accessibilityHint=""
onPress={() => {
setActiveToast(undefined)
}}
/>
</View>
)}
</>
)
}
// methods
// =
export function show(
text: string,
type: ToastType | LegacyToastType = 'default',
) {
if (toastTimeout) {
clearTimeout(toastTimeout)
}
globalSetActiveToast?.({text, type: convertLegacyToastType(type)})
toastTimeout = setTimeout(() => {
globalSetActiveToast?.(undefined)
}, DURATION)
}
const styles = StyleSheet.create({
container: {
// @ts-ignore web only
position: 'fixed',
left: 20,
bottom: 20,
// @ts-ignore web only
width: 'calc(100% - 40px)',
maxWidth: 380,
padding: 20,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 10,
borderWidth: 1,
},
dismissBackdrop: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
iconContainer: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
},
icon: {
flexShrink: 0,
},
text: {
marginLeft: 10,
},
})
+1 -1
View File
@@ -3,7 +3,7 @@ import {Header} from '#/components/Layout'
/** /**
* Legacy ViewHeader component. Use Layout.Header going forward. * Legacy ViewHeader component. Use Layout.Header going forward.
* *
* @deprecated * @deprecated use `Layout.Header` from `#/components/Layout.tsx`
*/ */
export function ViewHeader({ export function ViewHeader({
title, title,
+10 -8
View File
@@ -1,16 +1,16 @@
import React from 'react' import React from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
GestureResponderEvent, type GestureResponderEvent,
NativeSyntheticEvent, type NativeSyntheticEvent,
NativeTouchEvent, type NativeTouchEvent,
Pressable, Pressable,
PressableStateCallbackType, type PressableStateCallbackType,
StyleProp, type StyleProp,
StyleSheet, StyleSheet,
TextStyle, type TextStyle,
View, View,
ViewStyle, type ViewStyle,
} from 'react-native' } from 'react-native'
import {choose} from '#/lib/functions' import {choose} from '#/lib/functions'
@@ -37,7 +37,9 @@ declare module 'react-native' {
} }
} }
// TODO: Enforce that button always has a label /**
* @deprecated use Button from `#/components/Button.tsx` instead
*/
export function Button({ export function Button({
type = 'primary', type = 'primary',
label, label,
+12 -7
View File
@@ -1,22 +1,24 @@
import React, {PropsWithChildren, useMemo, useRef} from 'react' import {type PropsWithChildren} from 'react'
import {useMemo, useRef} from 'react'
import { import {
Dimensions, Dimensions,
GestureResponderEvent, type GestureResponderEvent,
Insets, type Insets,
StyleProp, type StyleProp,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
TouchableWithoutFeedback, TouchableWithoutFeedback,
useWindowDimensions, useWindowDimensions,
View, View,
ViewStyle, type ViewStyle,
} from 'react-native' } from 'react-native'
import Animated, {FadeIn, FadeInDown, FadeInUp} from 'react-native-reanimated' import Animated, {FadeIn, FadeInDown, FadeInUp} from 'react-native-reanimated'
import RootSiblings from 'react-native-root-siblings' import RootSiblings from 'react-native-root-siblings'
import {IconProp} from '@fortawesome/fontawesome-svg-core' import {type IconProp} from '@fortawesome/fontawesome-svg-core'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import type React from 'react'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
@@ -26,7 +28,7 @@ import {isWeb} from '#/platform/detection'
import {native} from '#/alf' import {native} from '#/alf'
import {FullWindowOverlay} from '#/components/FullWindowOverlay' import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {Button, ButtonType} from './Button' import {Button, type ButtonType} from './Button'
const ESTIMATED_BTN_HEIGHT = 50 const ESTIMATED_BTN_HEIGHT = 50
const ESTIMATED_SEP_HEIGHT = 16 const ESTIMATED_SEP_HEIGHT = 16
@@ -70,6 +72,9 @@ interface DropdownButtonProps {
accessibilityHint?: string accessibilityHint?: string
} }
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export function DropdownButton({ export function DropdownButton({
testID, testID,
type = 'bare', type = 'bare',
+34 -4
View File
@@ -1,9 +1,15 @@
import React from 'react' import React from 'react'
import {Platform, Pressable, StyleSheet, View, ViewStyle} from 'react-native' import {
import {IconProp} from '@fortawesome/fontawesome-svg-core' Platform,
Pressable,
StyleSheet,
View,
type ViewStyle,
} from 'react-native'
import {type IconProp} from '@fortawesome/fontawesome-svg-core'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import * as DropdownMenu from 'zeego/dropdown-menu' import * as DropdownMenu from 'zeego/dropdown-menu'
import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' import {type MenuItemCommonProps} from 'zeego/lib/typescript/menu'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {useTheme} from '#/lib/ThemeContext' import {useTheme} from '#/lib/ThemeContext'
@@ -12,8 +18,14 @@ import {Portal} from '#/components/Portal'
// Custom Dropdown Menu Components // Custom Dropdown Menu Components
// == // ==
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuRoot = DropdownMenu.Root export const DropdownMenuRoot = DropdownMenu.Root
// export const DropdownMenuTrigger = DropdownMenu.Trigger // export const DropdownMenuTrigger = DropdownMenu.Trigger
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuContent = DropdownMenu.Content export const DropdownMenuContent = DropdownMenu.Content
type TriggerProps = Omit< type TriggerProps = Omit<
@@ -25,6 +37,9 @@ type TriggerProps = Omit<
accessibilityLabel?: string accessibilityLabel?: string
accessibilityHint?: string accessibilityHint?: string
}> }>
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuTrigger = DropdownMenu.create( export const DropdownMenuTrigger = DropdownMenu.create(
(props: TriggerProps) => { (props: TriggerProps) => {
const theme = useTheme() const theme = useTheme()
@@ -59,6 +74,9 @@ export const DropdownMenuTrigger = DropdownMenu.create(
) )
type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']> type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']>
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuItem = DropdownMenu.create( export const DropdownMenuItem = DropdownMenu.create(
(props: ItemProps & {testID?: string}) => { (props: ItemProps & {testID?: string}) => {
const theme = useTheme() const theme = useTheme()
@@ -84,6 +102,9 @@ export const DropdownMenuItem = DropdownMenu.create(
) )
type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']> type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']>
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuItemTitle = DropdownMenu.create( export const DropdownMenuItemTitle = DropdownMenu.create(
(props: TitleProps) => { (props: TitleProps) => {
const pal = usePalette('default') const pal = usePalette('default')
@@ -98,11 +119,17 @@ export const DropdownMenuItemTitle = DropdownMenu.create(
) )
type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']> type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']>
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => { export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => {
return <DropdownMenu.ItemIcon {...props} /> return <DropdownMenu.ItemIcon {...props} />
}, 'ItemIcon') }, 'ItemIcon')
type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']> type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']>
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuSeparator = DropdownMenu.create( export const DropdownMenuSeparator = DropdownMenu.create(
(props: SeparatorProps) => { (props: SeparatorProps) => {
const pal = usePalette('default') const pal = usePalette('default')
@@ -142,11 +169,14 @@ type Props = {
triggerStyle?: ViewStyle triggerStyle?: ViewStyle
} }
/* The `NativeDropdown` function uses native iOS and Android dropdown menus. /**
* The `NativeDropdown` function uses native iOS and Android dropdown menus.
* It also creates a animated custom dropdown for web that uses * It also creates a animated custom dropdown for web that uses
* Radix UI primitives under the hood * Radix UI primitives under the hood
* @prop {DropdownItem[]} items - An array of dropdown items * @prop {DropdownItem[]} items - An array of dropdown items
* @prop {React.ReactNode} children - A custom dropdown trigger * @prop {React.ReactNode} children - A custom dropdown trigger
*
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/ */
export function NativeDropdown({ export function NativeDropdown({
items, items,
@@ -63,6 +63,9 @@ type Props = {
triggerStyle?: ViewStyle triggerStyle?: ViewStyle
} }
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export function NativeDropdown({ export function NativeDropdown({
items, items,
children, children,
+12 -3
View File
@@ -1,12 +1,21 @@
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' import {
type StyleProp,
StyleSheet,
type TextStyle,
View,
type ViewStyle,
} from 'react-native'
import {choose} from '#/lib/functions' import {choose} from '#/lib/functions'
import {colors} from '#/lib/styles' import {colors} from '#/lib/styles'
import {useTheme} from '#/lib/ThemeContext' import {useTheme} from '#/lib/ThemeContext'
import {TypographyVariant} from '#/lib/ThemeContext' import {type TypographyVariant} from '#/lib/ThemeContext'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {Button, ButtonType} from './Button' import {Button, type ButtonType} from './Button'
/**
* @deprecated use Toggle from `#/components/form/Toggle.tsx` instead
*/
export function ToggleButton({ export function ToggleButton({
testID, testID,
type = 'default-light', type = 'default-light',
+4 -4
View File
@@ -1,16 +1,16 @@
import React from 'react' import React from 'react'
import {StyleSheet, TextProps} from 'react-native' import {StyleSheet, type TextProps} from 'react-native'
import {UITextView} from 'react-native-uitextview' import {UITextView} from 'react-native-uitextview'
import {lh, s} from '#/lib/styles' import {lh, s} from '#/lib/styles'
import {TypographyVariant, useTheme} from '#/lib/ThemeContext' import {type TypographyVariant, useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isIOS, isWeb} from '#/platform/detection' import {isIOS, isWeb} from '#/platform/detection'
import {applyFonts, useAlf} from '#/alf' import {applyFonts, useAlf} from '#/alf'
import { import {
childHasEmoji, childHasEmoji,
renderChildrenWithEmoji, renderChildrenWithEmoji,
StringChild, type StringChild,
} from '#/alf/typography' } from '#/alf/typography'
export type CustomTextProps = Omit<TextProps, 'children'> & { export type CustomTextProps = Omit<TextProps, 'children'> & {
@@ -32,7 +32,7 @@ export type CustomTextProps = Omit<TextProps, 'children'> & {
export {Text_DEPRECATED as Text} export {Text_DEPRECATED as Text}
/** /**
* @deprecated use Text from Typography instead. * @deprecated use Text from `#/components/Typography.tsx` instead
*/ */
function Text_DEPRECATED({ function Text_DEPRECATED({
type = 'md', type = 'md',
+43 -167
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
@@ -7,7 +6,6 @@ import {
type ButtonColor, type ButtonColor,
ButtonIcon, ButtonIcon,
ButtonText, ButtonText,
type ButtonVariant,
} from '#/components/Button' } from '#/components/Button'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
@@ -27,258 +25,136 @@ export function Buttons() {
'negative_secondary', 'negative_secondary',
].map(color => ( ].map(color => (
<View key={color} style={[a.gap_md, a.align_start]}> <View key={color} style={[a.gap_md, a.align_start]}>
{['solid', 'outline', 'ghost'].map(variant => ( <Button
<React.Fragment key={variant}> color={color as ButtonColor}
<Button size="large"
variant={variant as ButtonVariant} label="Click here">
color={color as ButtonColor} <ButtonText>Button</ButtonText>
size="large" </Button>
label="Click here"> <Button
<ButtonText>Button</ButtonText> disabled
</Button> color={color as ButtonColor}
<Button size="large"
disabled label="Click here">
variant={variant as ButtonVariant} <ButtonText>Button</ButtonText>
color={color as ButtonColor} </Button>
size="large"
label="Click here">
<ButtonText>Button</ButtonText>
</Button>
</React.Fragment>
))}
</View> </View>
))} ))}
<View style={[a.flex_row, a.gap_md, a.align_start]}>
<View style={[a.gap_md, a.align_start]}>
{['gradient_sky', 'gradient_midnight', 'gradient_sunrise'].map(
name => (
<React.Fragment key={name}>
<Button
variant="gradient"
color={name as ButtonColor}
size="large"
label="Click here">
<ButtonText>Button</ButtonText>
</Button>
<Button
disabled
variant="gradient"
color={name as ButtonColor}
size="large"
label="Click here">
<ButtonText>Button</ButtonText>
</Button>
</React.Fragment>
),
)}
</View>
</View>
</View> </View>
<View style={[a.flex_wrap, a.gap_md, a.align_start]}> <View style={[a.flex_wrap, a.gap_md, a.align_start]}>
<Button variant="solid" color="primary" size="large" label="Link out"> <Button color="primary" size="large" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
</Button> </Button>
<Button variant="solid" color="primary" size="large" label="Link out"> <Button color="primary" size="large" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" /> <ButtonIcon icon={Globe} position="right" />
</Button> </Button>
<Button variant="solid" color="primary" size="small" label="Link out"> <Button color="primary" size="small" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
</Button> </Button>
<Button variant="solid" color="primary" size="small" label="Link out"> <Button color="primary" size="small" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" /> <ButtonIcon icon={Globe} position="right" />
</Button> </Button>
<Button variant="solid" color="primary" size="tiny" label="Link out"> <Button color="primary" size="tiny" label="Link out">
<ButtonIcon icon={Globe} position="left" /> <ButtonIcon icon={Globe} position="left" />
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
</Button> </Button>
</View> </View>
<View style={[a.flex_row, a.gap_md, a.align_center]}> <View style={[a.flex_row, a.gap_md, a.align_center]}>
<Button variant="solid" color="primary" size="large" label="Link out"> <Button color="primary" size="large" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
</Button> </Button>
<Button variant="solid" color="primary" size="large" label="Link out"> <Button color="primary" size="large" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" /> <ButtonIcon icon={Globe} position="right" />
</Button> </Button>
<Button variant="solid" color="primary" size="large" label="Link out"> <Button color="primary" size="large" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" size="lg" /> <ButtonIcon icon={Globe} position="right" size="lg" />
</Button> </Button>
<Button <Button color="primary" size="large" shape="round" label="Link out">
variant="solid"
color="primary"
size="large"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="large" shape="round" label="Link out">
variant="solid"
color="primary"
size="large"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} size="lg" /> <ButtonIcon icon={ChevronLeft} size="lg" />
</Button> </Button>
</View> </View>
<View style={[a.flex_row, a.gap_md, a.align_center]}> <View style={[a.flex_row, a.gap_md, a.align_center]}>
<Button variant="solid" color="primary" size="small" label="Link out"> <Button color="primary" size="small" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
</Button> </Button>
<Button variant="solid" color="primary" size="small" label="Link out"> <Button color="primary" size="small" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" /> <ButtonIcon icon={Globe} position="right" />
</Button> </Button>
<Button <Button color="primary" size="small" shape="round" label="Link out">
variant="solid"
color="primary"
size="small"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="small" shape="round" label="Link out">
variant="solid"
color="primary"
size="small"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} size="lg" /> <ButtonIcon icon={ChevronLeft} size="lg" />
</Button> </Button>
</View> </View>
<View style={[a.flex_row, a.gap_md, a.align_center]}> <View style={[a.flex_row, a.gap_md, a.align_center]}>
<Button variant="solid" color="primary" size="tiny" label="Link out"> <Button color="primary" size="tiny" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
</Button> </Button>
<Button variant="solid" color="primary" size="tiny" label="Link out"> <Button color="primary" size="tiny" label="Link out">
<ButtonText>Button</ButtonText> <ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" /> <ButtonIcon icon={Globe} position="right" />
</Button> </Button>
<Button <Button color="primary" size="tiny" shape="round" label="Link out">
variant="solid"
color="primary"
size="tiny"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="tiny" shape="round" label="Link out">
variant="solid"
color="primary"
size="tiny"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} size="md" /> <ButtonIcon icon={ChevronLeft} size="md" />
</Button> </Button>
</View> </View>
<View style={[a.flex_row, a.gap_md, a.align_center]}> <View style={[a.flex_row, a.gap_md, a.align_center]}>
<Button <Button color="primary" size="large" shape="round" label="Link out">
variant="solid"
color="primary"
size="large"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="small" shape="round" label="Link out">
variant="gradient"
color="gradient_sunset"
size="small"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="tiny" shape="round" label="Link out">
variant="gradient"
color="gradient_sunset"
size="tiny"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="large" shape="round" label="Link out">
variant="outline"
color="primary"
size="large"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="small" shape="round" label="Link out">
variant="ghost"
color="primary"
size="small"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="tiny" shape="round" label="Link out">
variant="ghost"
color="primary"
size="tiny"
shape="round"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
</View> </View>
<View style={[a.flex_row, a.gap_md, a.align_start]}> <View style={[a.flex_row, a.gap_md, a.align_start]}>
<Button <Button color="primary" size="large" shape="square" label="Link out">
variant="solid"
color="primary"
size="large"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="small" shape="square" label="Link out">
variant="gradient"
color="gradient_sunset"
size="small"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="tiny" shape="square" label="Link out">
variant="gradient"
color="gradient_sunset"
size="tiny"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="large" shape="square" label="Link out">
variant="outline"
color="primary"
size="large"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="small" shape="square" label="Link out">
variant="ghost"
color="primary"
size="small"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
<Button <Button color="primary" size="tiny" shape="square" label="Link out">
variant="ghost"
color="primary"
size="tiny"
shape="square"
label="Link out">
<ButtonIcon icon={ChevronLeft} /> <ButtonIcon icon={ChevronLeft} />
</Button> </Button>
</View> </View>
+3 -3
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {TextInput, View} from 'react-native' import {type TextInput, View} from 'react-native'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
@@ -216,8 +216,8 @@ export function Forms() {
</View> </View>
<Button <Button
variant="gradient" variant="solid"
color="gradient_nordic" color="primary"
size="small" size="small"
label="Reset all toggles" label="Reset all toggles"
onPress={() => { onPress={() => {
+101 -87
View File
@@ -1,65 +1,11 @@
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import * as Toast from '#/view/com/util/Toast' import {show as deprecatedShow} from '#/view/com/util/Toast'
import { import {atoms as a} from '#/alf'
getToastTypeStyles, import {Button, ButtonText} from '#/components/Button'
TOAST_TYPE_TO_ICON, import {toast} from '#/components/Toast'
type ToastType, import {Toast} from '#/components/Toast/Toast'
} from '#/view/com/util/Toast.style' import {H1} from '#/components/Typography'
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() { export function Toasts() {
return ( return (
@@ -67,35 +13,103 @@ export function Toasts() {
<H1>Toast Examples</H1> <H1>Toast Examples</H1>
<View style={[a.gap_md]}> <View style={[a.gap_md]}>
<View style={[a.gap_xs]}> <Pressable
<ToastPreview message="Default Toast" type="default" /> accessibilityRole="button"
</View> onPress={() =>
toast.show({
<View style={[a.gap_xs]}> type: 'default',
<ToastPreview content: 'Default toast',
message="Operation completed successfully!" a11yLabel: 'Default toast',
type="success" })
}>
<Toast content="Default toast" type="default" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'default',
content: 'Default toast, 6 seconds',
a11yLabel: 'Default toast, 6 seconds',
duration: 6e3,
})
}>
<Toast content="Default toast, 6 seconds" type="default" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'default',
content:
'This is a longer message to test how the toast handles multiple lines of text content.',
a11yLabel:
'This is a longer message to test how the toast handles multiple lines of text content.',
})
}>
<Toast
content="This is a longer message to test how the toast handles multiple lines of text content."
type="default"
/> />
</View> </Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'success',
content: 'Success toast',
a11yLabel: 'Success toast',
})
}>
<Toast content="Success toast" type="success" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'info',
content: 'Info toast',
a11yLabel: 'Info toast',
})
}>
<Toast content="Info" type="info" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'warning',
content: 'Warning toast',
a11yLabel: 'Warning toast',
})
}>
<Toast content="Warning" type="warning" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'error',
content: 'Error toast',
a11yLabel: 'Error toast',
})
}>
<Toast content="Error" type="error" />
</Pressable>
<View style={[a.gap_xs]}> <Button
<ToastPreview message="Something went wrong!" type="error" /> label="Deprecated toast example"
</View> onPress={() =>
deprecatedShow(
<View style={[a.gap_xs]}> 'This is a deprecated toast example',
<ToastPreview message="Please check your input" type="warning" /> 'exclamation-circle',
</View> )
}
<View style={[a.gap_xs]}> size="large"
<ToastPreview message="Here's some helpful information" type="info" /> variant="solid"
</View> color="secondary">
<ButtonText>Deprecated toast example</ButtonText>
<View style={[a.gap_xs]}> </Button>
<ToastPreview
message="This is a longer message to test how the toast handles multiple lines of text content."
type="info"
/>
</View>
</View> </View>
</View> </View>
) )
+2 -8
View File
@@ -52,7 +52,6 @@ function StorybookInner() {
<> <>
<View style={[a.flex_row, a.align_start, a.gap_md]}> <View style={[a.flex_row, a.align_start, a.gap_md]}>
<Button <Button
variant="outline"
color="primary" color="primary"
size="small" size="small"
label='Set theme to "system"' label='Set theme to "system"'
@@ -60,7 +59,6 @@ function StorybookInner() {
<ButtonText>System</ButtonText> <ButtonText>System</ButtonText>
</Button> </Button>
<Button <Button
variant="solid"
color="secondary" color="secondary"
size="small" size="small"
label='Set theme to "light"' label='Set theme to "light"'
@@ -68,7 +66,6 @@ function StorybookInner() {
<ButtonText>Light</ButtonText> <ButtonText>Light</ButtonText>
</Button> </Button>
<Button <Button
variant="solid"
color="secondary" color="secondary"
size="small" size="small"
label='Set theme to "dim"' label='Set theme to "dim"'
@@ -79,7 +76,6 @@ function StorybookInner() {
<ButtonText>Dim</ButtonText> <ButtonText>Dim</ButtonText>
</Button> </Button>
<Button <Button
variant="solid"
color="secondary" color="secondary"
size="small" size="small"
label='Set theme to "dark"' label='Set theme to "dark"'
@@ -91,8 +87,9 @@ function StorybookInner() {
</Button> </Button>
</View> </View>
<Toasts />
<Button <Button
variant="solid"
color="primary" color="primary"
size="small" size="small"
onPress={() => navigation.navigate('SharedPreferencesTester')} onPress={() => navigation.navigate('SharedPreferencesTester')}
@@ -123,11 +120,9 @@ function StorybookInner() {
<Breakpoints /> <Breakpoints />
<Dialogs /> <Dialogs />
<Admonitions /> <Admonitions />
<Toasts />
<Settings /> <Settings />
<Button <Button
variant="solid"
color="primary" color="primary"
size="large" size="large"
label="Switch to Contained List" label="Switch to Contained List"
@@ -138,7 +133,6 @@ function StorybookInner() {
) : ( ) : (
<> <>
<Button <Button
variant="solid"
color="primary" color="primary"
size="large" size="large"
label="Switch to Storybook" label="Switch to Storybook"
+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,
}, },