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=
SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG=
# This is the semver release version of the app, pulled from package.json
EXPO_PUBLIC_RELEASE_VERSION=
# This is the commit hash that the current bundle was made from.
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
# Should be formatted YYMMDDHH so that it increases for each build.
EXPO_PUBLIC_BUNDLE_DATE=0
# The log level for the app's logger transports
EXPO_PUBLIC_LOG_LEVEL=debug
# Enable debug logs for specific logger instances
EXPO_PUBLIC_LOG_DEBUG=session
# Chat service DID
EXPO_PUBLIC_CHAT_PROXY_DID=
#
#
# Bluesky specific values
#
#
# Sentry DSN for telemetry
EXPO_PUBLIC_SENTRY_DSN=
# Bitdrift API key. If undefined, Bitdrift will be disabled.
EXPO_PUBLIC_BITDRIFT_API_KEY=
@@ -43,12 +43,11 @@ jobs:
tags: |
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
- name: Set outputs
id: vars
- name: Env
id: env
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Build and push Docker image
id: build-and-push
@@ -62,8 +61,8 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.vars.outputs.sha_short }}
SENTRY_DIST=${{ steps.vars.outputs.SENTRY_DIST }}
SENTRY_RELEASE=${{ steps.vars.outputs.SENTRY_RELEASE }}
EXPO_PUBLIC_ENV=production
EXPO_PUBLIC_RELEASE_VERSION=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
@@ -3,6 +3,7 @@ on:
push:
branches:
- main
- echoprom_fix
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
+25 -11
View File
@@ -62,23 +62,30 @@ jobs:
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
- name: ✏️ Write environment variables
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "$json" > google-services.json
- name: Setup Sentry vars for build-time injection
id: sentry
run: |
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
- name: 🏗️ EAS Build
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p android
--profile ${{ inputs.profile || 'testflight-android' }}
--local --output build.aab --non-interactive
- name: ✍️ Rename Testflight bundle
if: ${{ inputs.profile != 'production' }}
@@ -140,7 +147,14 @@ jobs:
- name: 🏗️ Build Production APK
if: ${{ inputs.profile == 'production' }}
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p android --profile production-apk --local --output build.apk --non-interactive
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p android
--profile production-apk
--local --output build.apk --non-interactive
- name: 🚀 Upload Production APK Artifact
id: upload-artifact-production-apk
+16 -9
View File
@@ -75,22 +75,29 @@ jobs:
- name: Check for i18n compilation errors
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
# EXPO_PUBLIC_ENV is handled in eas.json
- name: ✏️ Write environment variables
id: env
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: Setup Sentry vars for build-time injection
id: sentry
run: |
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
- name: 🏗️ EAS Build
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p ios
--profile ${{ inputs.profile || 'testflight' }}
--local --output build.ipa --non-interactive
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
+49 -18
View File
@@ -101,25 +101,30 @@ jobs:
if: ${{ !steps.fingerprint.outputs.includes-changes }}
uses: dcarbone/install-jq-action@v2
- name: ✏️ Write environment variables
# eas.json not used here, set EXPO_PUBLIC_ENV
- name: Env
id: env
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_ENV=${{ inputs.channel || 'testflight' }}" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "$json" > google-services.json
- name: Setup Sentry vars for build-time injection
id: sentry
run: |
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
- name: 🏗️ Create Bundle
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn export
- name: 📦 Package Bundle and 🚀 Deploy
if: ${{ !steps.fingerprint.outputs.includes-changes }}
@@ -205,16 +210,29 @@ jobs:
- name: 🔤 Compile translations
run: yarn intl:build
- name: ✏️ Write environment variables
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p ios
--profile testflight
--local --output build.ipa --non-interactive
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
@@ -282,17 +300,30 @@ jobs:
- name: 🔤 Compile translations
run: yarn intl:build
- name: ✏️ Write environment variables
# EXPO_PUBLIC_ENV is handled in eas.json
- name: Env
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "$json" > google-services.json
- name: 🏗️ EAS Build
run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn use-build-number-with-bump
eas build -p android
--profile testflight-android
--local --output build.apk --non-interactive
- name: ⏰ Get a timestamp
id: timestamp
+14 -10
View File
@@ -152,23 +152,27 @@ jobs:
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: ✏️ Write environment variables
- name: Env
id: env
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_ENV=testflight" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "$json" > google-services.json
- name: Setup Sentry vars for build-time injection
id: sentry
run: |
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
- name: 🏗️ Create Bundle
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="testflight" yarn export
run: >
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
yarn export
- name: 📦 Package Bundle and 🚀 Deploy
run: yarn use-build-number bash scripts/bundleUpdate.sh
+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
@@ -19,28 +19,28 @@ ENV GOARCH="amd64"
ENV CGO_ENABLED=1
ENV GOEXPERIMENT="loopvar"
# The latest git hash of the preview branch on render.com
# https://render.com/docs/docker-secrets#environment-variables-in-docker-builds
ARG RENDER_GIT_COMMIT
#
# Expo
#
ARG EXPO_PUBLIC_ENV
ENV EXPO_PUBLIC_ENV=${EXPO_PUBLIC_ENV:-development}
ARG EXPO_PUBLIC_RELEASE_VERSION
ENV EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION
ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev}
# The latest git hash of the preview branch on render.com
ARG RENDER_GIT_COMMIT
# If not set by GitHub workflows, we're probably in Render
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-$RENDER_GIT_COMMIT}
#
# Sentry
#
ARG SENTRY_AUTH_TOKEN
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown}
# Will fall back to package.json#version, but this is handled elsewhere
ARG SENTRY_RELEASE
ENV SENTRY_RELEASE=$SENTRY_RELEASE
ARG SENTRY_DIST
# Default to RENDER_GIT_COMMIT if not set by GitHub workflows
ENV SENTRY_DIST=${SENTRY_DIST:-$RENDER_GIT_COMMIT}
ARG SENTRY_DSN
ENV SENTRY_DSN=$SENTRY_DSN
ARG EXPO_PUBLIC_SENTRY_DSN
ENV EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN
#
# Copy everything into the container
@@ -60,13 +60,16 @@ RUN \. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION && \
nvm use $NODE_VERSION && \
echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \
echo "EXPO_PUBLIC_ENV=$EXPO_PUBLIC_ENV" >> .env && \
echo "EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION" >> .env && \
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env && \
echo "EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN" >> .env && \
npm install --global yarn && \
yarn && \
yarn intl:build 2>&1 | tee i18n.log && \
if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compile errors!\n\n"; fi && \
EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER EXPO_PUBLIC_BUNDLE_DATE=$() SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$SENTRY_RELEASE SENTRY_DIST=$SENTRY_DIST SENTRY_DSN=$SENTRY_DSN yarn build-web
SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$EXPO_PUBLIC_RELEASE_VERSION SENTRY_DIST=$EXPO_PUBLIC_BUNDLE_IDENTIFIER yarn build-web
# DEBUG
RUN find ./bskyweb/static && find ./web-build/static
+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
+4 -3
View File
@@ -10,7 +10,7 @@ Get the app itself:
## 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.
@@ -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:
- [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)
- [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
> [!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.
**Rules:**
@@ -59,7 +60,7 @@ Please be sure to:
## 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?
+7 -1
View File
@@ -154,7 +154,13 @@ func serve(cctx *cli.Context) error {
RedirectCode: http.StatusFound,
}))
e.Use(echoprometheus.NewMiddleware(""))
echoprom := echoprometheus.NewMiddlewareWithConfig(
echoprometheus.MiddlewareConfig{
DoNotUseRequestPathFor404: true,
},
)
e.Use(echoprom)
//
// configure routes
+12 -14
View File
@@ -1,8 +1,6 @@
module github.com/bluesky-social/social-app/bskyweb
go 1.24
toolchain go1.24.5
go 1.24.5
require (
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/joho/godotenv v1.5.1
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
)
@@ -53,7 +53,6 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/lestrrat-go/blackmagic 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/orandin/slog-gorm v1.3.2 // 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/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/asm v1.2.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/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/protobuf v1.36.6 // 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/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/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
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/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
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_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
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.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
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-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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
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/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
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.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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
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-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-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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
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-20190412213103-97732733099d/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.8.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.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
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-20190311212946-11955173bddd/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 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
+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 HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
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 ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
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 {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
+270 -296
View File
@@ -14,31 +14,36 @@ import {
View,
type ViewStyle,
} 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 {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 =
| 'primary'
| 'secondary'
| 'secondary_inverted'
| 'negative'
| 'negative_secondary'
| 'gradient_primary'
| 'gradient_sky'
| 'gradient_midnight'
| 'gradient_sunrise'
| 'gradient_sunset'
| 'gradient_nordic'
| 'gradient_bonfire'
export type ButtonSize = 'tiny' | 'small' | 'large'
export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = {
/**
* The style variation of the button
* @deprecated Use `color` instead.
*/
variant?: ButtonVariant
/**
@@ -131,6 +136,15 @@ export const Button = React.forwardRef<View, ButtonProps>(
},
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 [state, setState] = React.useState({
pressed: false,
@@ -203,8 +217,13 @@ export const Button = React.forwardRef<View, ButtonProps>(
const baseStyles: 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) {
baseStyles.push({
backgroundColor: t.palette.primary_500,
@@ -221,64 +240,14 @@ export const Button = React.forwardRef<View, ButtonProps>(
}),
})
}
} else if (variant === 'outline') {
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') {
} else if (color === 'secondary') {
if (!disabled) {
baseStyles.push(t.atoms.bg_contrast_25)
hoverStyles.push(t.atoms.bg_contrast_50)
} else {
baseStyles.push(t.atoms.bg_contrast_100)
}
} else 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 === 'secondary_inverted') {
if (variant === 'solid') {
} else if (color === 'secondary_inverted') {
if (!disabled) {
baseStyles.push({
backgroundColor: t.palette.contrast_900,
@@ -291,31 +260,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
backgroundColor: t.palette.contrast_600,
})
}
} else 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 === 'solid') {
} else if (color === 'negative') {
if (!disabled) {
baseStyles.push({
backgroundColor: t.palette.negative_500,
@@ -332,33 +277,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
}),
})
}
} else 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 === 'solid') {
} else if (color === 'negative_secondary') {
if (!disabled) {
baseStyles.push({
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, {
borderWidth: 1,
})
}
} else {
/*
* BEGIN DEPRECATED STYLES
*/
if (color === 'primary') {
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,
})
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 (variant === 'ghost') {
if (!disabled) {
baseStyles.push(t.atoms.bg)
hoverStyles.push({
backgroundColor: t.palette.negative_100,
} else if (color === 'secondary') {
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 === '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') {
@@ -471,49 +500,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
}
}, [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>(
() => ({
...state,
@@ -556,27 +542,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
onHoverOut={onHoverOut}
onFocus={onFocus}
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}>
{typeof children === 'function' ? children(context) : children}
</Context.Provider>
@@ -592,30 +557,19 @@ export function useSharedButtonTextStyles() {
return React.useMemo(() => {
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) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, opacity: 0.5})
}
} else 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 === 'solid' || variant === 'gradient') {
} else if (color === 'secondary') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_700,
@@ -625,29 +579,7 @@ export function useSharedButtonTextStyles() {
color: t.palette.contrast_400,
})
}
} else 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 === 'solid' || variant === 'gradient') {
} else if (color === 'secondary_inverted') {
if (!disabled) {
baseStyles.push({
color: t.palette.contrast_50,
@@ -657,49 +589,13 @@ export function useSharedButtonTextStyles() {
color: t.palette.contrast_400,
})
}
} else 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 === 'solid' || variant === 'gradient') {
} else if (color === 'negative') {
if (!disabled) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, 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 if (color === 'negative_secondary') {
if (variant === 'solid' || variant === 'gradient') {
} else if (color === 'negative_secondary') {
if (!disabled) {
baseStyles.push({
color: select(t.name, {
@@ -718,25 +614,103 @@ export function useSharedButtonTextStyles() {
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 {
if (!disabled) {
baseStyles.push({color: t.palette.white})
} else {
baseStyles.push({color: t.palette.white, opacity: 0.5})
/*
* BEGIN DEPRECATED STYLES
*/
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') {
+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]}>
<View
style={[
a.p_lg,
a.py_md,
a.px_lg,
a.pt_md,
a.flex_row,
a.align_center,
a.justify_between,
@@ -399,7 +399,7 @@ export function ProfileGrid({
</View>
{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]}>
{content}
</View>
@@ -411,9 +411,8 @@ export function ProfileGrid({
horizontal
showsHorizontalScrollIndicator={false}
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
decelerationRate="fast"
style={[a.overflow_visible]}>
<View style={[a.px_lg, a.pb_lg, a.flex_row, a.gap_md]}>
decelerationRate="fast">
<View style={[a.p_lg, a.pt_md, a.flex_row, a.gap_md]}>
{content}
<SeeMoreSuggestedProfilesCard />
@@ -434,6 +433,7 @@ function SeeMoreSuggestedProfilesCard() {
return (
<Button
label={_(msg`Browse more accounts on the Explore page`)}
style={[a.flex_col]}
onPress={() => {
navigation.navigate('SearchTab')
}}>
+4 -3
View File
@@ -1,7 +1,8 @@
import React, {memo} from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {memo} from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import type React from 'react'
import {cleanError} from '#/lib/strings/errors'
import {CenteredView} from '#/view/com/util/Views'
@@ -95,7 +96,7 @@ function ListFooterMaybeError({
)}
</Text>
<Button
variant="gradient"
variant="solid"
label={_(msg`Press to retry`)}
style={[
a.align_center,
@@ -2,13 +2,13 @@ import {Pressable} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {t} from '@lingui/macro'
import {IS_INTERNAL} from '#/lib/app-info'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {useGate} from '#/lib/statsig/statsig'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {IS_INTERNAL} from '#/env'
export function DiscoverDebug({
feedContext,
@@ -17,7 +17,6 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {IS_INTERNAL} from '#/lib/app-info'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {useOpenLink} from '#/lib/hooks/useOpenLink'
import {getCurrentRoute} from '#/lib/routes/helpers'
@@ -83,6 +82,7 @@ import {
useReportDialogControl,
} from '#/components/moderation/ReportDialog'
import * as Prompt from '#/components/Prompt'
import {IS_INTERNAL} from '#/env'
import * as bsky from '#/types/bsky'
let PostMenuItems = ({
+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 {TextStyle, ViewStyle} from 'react-native'
import {type ReactNode} from 'react'
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'
export type ColorScheme = 'light' | 'dark'
@@ -29,6 +30,9 @@ export type Palette = Record<PaletteColorName, PaletteColor>
export type ShapeName = 'button' | 'bigButton' | 'smallButton'
export type Shapes = Record<ShapeName, ViewStyle>
/**
* @deprecated use typography atoms from `#/alf`
*/
export type TypographyVariant =
| '2xl-thin'
| '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,
} from 'expo-updates'
import {IS_TESTFLIGHT} from '#/lib/app-info'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {IS_TESTFLIGHT} from '#/env'
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
+10 -2
View File
@@ -1,7 +1,11 @@
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 {
colors: PaletteColor
@@ -16,6 +20,10 @@ export interface UsePaletteValue {
link: TextStyle
icon: TextStyle
}
/**
* @deprecated use `useTheme` from `#/alf`
*/
export function usePalette(color: PaletteColorName): UsePaletteValue {
const theme = useTheme()
return useMemo(() => {
+9 -11
View File
@@ -3,12 +3,11 @@ import {Platform} from 'react-native'
import {AppState, type AppStateStatus} from 'react-native'
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
import {logger} from '#/logger'
import {type MetricEvents} from '#/logger/metrics'
import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted'
import packageDotJson from '../../../package.json'
import * as env from '#/env'
import {useSession} from '../../state/session'
import {timeout} from '../async/timeout'
import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback'
@@ -49,12 +48,11 @@ export type {MetricEvents as LogEvents}
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
return {
environment: {
tier:
process.env.NODE_ENV === 'development'
? 'development'
: IS_TESTFLIGHT
? 'staging'
: 'production',
tier: env.IS_DEV
? 'development'
: env.IS_TESTFLIGHT
? 'staging'
: 'production',
},
// Don't block on waiting for network. The fetched config will kick in on next load.
// This ensures the UI is always consistent and doesn't update mid-session.
@@ -212,9 +210,9 @@ function toStatsigUser(did: string | undefined): StatsigUser {
refSrc,
refUrl,
platform: Platform.OS as 'ios' | 'android' | 'web',
appVersion: packageDotJson.version,
bundleIdentifier: BUNDLE_IDENTIFIER,
bundleDate: BUNDLE_DATE,
appVersion: env.RELEASE_VERSION,
bundleIdentifier: env.BUNDLE_IDENTIFIER,
bundleDate: env.BUNDLE_DATE,
appLanguage: languagePrefs.appLanguage,
contentLanguages: languagePrefs.contentLanguages,
},
+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 {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
/**
* @deprecated use ALF colors instead
*/
export const colors = {
white: '#ffffff',
black: '#000000',
@@ -63,6 +71,9 @@ export const gradients = {
blueDark: {start: '#5F45E0', end: colors.blue3}, // avis, banner
}
/**
* @deprecated use atoms from `#/alf`
*/
export const s = StyleSheet.create({
// helpers
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:144
#: src/view/com/composer/GifAltText.tsx:210
#: src/view/com/composer/photos/Gallery.tsx:169
#: src/view/com/composer/photos/Gallery.tsx:216
#: src/view/com/composer/photos/Gallery.tsx:170
#: src/view/com/composer/photos/Gallery.tsx:217
#: src/view/com/composer/photos/ImageAltTextDialog.tsx:88
#: src/view/com/composer/photos/ImageAltTextDialog.tsx:93
msgid "Add alt text"
@@ -870,7 +870,7 @@ msgstr ""
#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:186
#: 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"
msgstr ""
@@ -888,7 +888,7 @@ msgstr ""
msgid "Alt Text"
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."
msgstr ""
@@ -1239,7 +1239,7 @@ msgstr ""
#: src/components/dms/dialogs/NewChatDialog.tsx:54
#: src/components/dms/MessageProfileButton.tsx:58
#: src/screens/Messages/ChatList.tsx:362
#: src/screens/Messages/ChatList.tsx:358
#: src/screens/Messages/Conversation.tsx:228
msgid "Before you can message another user, you must first verify your email."
msgstr ""
@@ -1423,7 +1423,7 @@ msgstr ""
msgid "Books"
msgstr ""
#: src/components/FeedInterstitials.tsx:436
#: src/components/FeedInterstitials.tsx:435
msgid "Browse more accounts on the Explore page"
msgstr ""
@@ -1663,11 +1663,11 @@ msgid "Chat muted"
msgstr ""
#: src/Navigation.tsx:558
#: src/screens/Messages/components/InboxPreview.tsx:24
#: src/screens/Messages/components/InboxPreview.tsx:22
msgid "Chat request inbox"
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:98
msgid "Chat requests"
@@ -1675,7 +1675,7 @@ msgstr ""
#: src/components/dms/ConvoMenu.tsx:75
#: src/Navigation.tsx:553
#: src/screens/Messages/ChatList.tsx:371
#: src/screens/Messages/ChatList.tsx:367
msgid "Chat settings"
msgstr ""
@@ -1690,8 +1690,8 @@ msgid "Chat unmuted"
msgstr ""
#: src/screens/Messages/ChatList.tsx:76
#: src/screens/Messages/ChatList.tsx:387
#: src/screens/Messages/ChatList.tsx:411
#: src/screens/Messages/ChatList.tsx:383
#: src/screens/Messages/ChatList.tsx:407
msgid "Chats"
msgstr ""
@@ -2309,12 +2309,9 @@ msgstr ""
msgid "Create Account"
msgstr ""
#: src/screens/Search/SearchResults.tsx:266
msgid "create an account"
msgstr ""
#: src/components/dialogs/Signin.tsx:86
#: src/components/dialogs/Signin.tsx:88
#: src/screens/Search/SearchResults.tsx:266
msgid "Create an account"
msgstr ""
@@ -2666,6 +2663,10 @@ msgstr ""
msgid "Dismiss this section"
msgstr ""
#: src/components/Toast/index.web.tsx:81
msgid "Dismiss toast"
msgstr ""
#: src/screens/Settings/AccessibilitySettings.tsx:69
#: src/screens/Settings/AccessibilitySettings.tsx:74
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:89
#: src/view/com/composer/photos/Gallery.tsx:194
#: src/view/com/composer/photos/Gallery.tsx:195
msgid "Edit image"
msgstr ""
@@ -3339,7 +3340,7 @@ msgstr ""
msgid "Failed to delete starter pack"
msgstr ""
#: src/screens/Messages/ChatList.tsx:274
#: src/screens/Messages/ChatList.tsx:270
#: src/screens/Messages/Inbox.tsx:208
msgid "Failed to load conversations"
msgstr ""
@@ -5300,8 +5301,8 @@ msgid "New {postsCount, plural, one {post} other {posts}} from {firstAuthorName}
msgstr ""
#: src/components/dms/dialogs/NewChatDialog.tsx:67
#: src/screens/Messages/ChatList.tsx:394
#: src/screens/Messages/ChatList.tsx:401
#: src/screens/Messages/ChatList.tsx:390
#: src/screens/Messages/ChatList.tsx:397
msgid "New chat"
msgstr ""
@@ -5515,7 +5516,7 @@ msgstr ""
msgid "No results for \"{0}\"."
msgstr ""
#: src/components/Lists.tsx:189
#: src/components/Lists.tsx:190
msgid "No results found"
msgstr ""
@@ -5592,7 +5593,7 @@ msgstr ""
msgid "Note: This post is only visible to logged-in users."
msgstr ""
#: src/screens/Messages/ChatList.tsx:295
#: src/screens/Messages/ChatList.tsx:291
msgid "Nothing here"
msgstr ""
@@ -5740,11 +5741,11 @@ msgstr ""
msgid "Only WebVTT (.vtt) files are supported"
msgstr ""
#: src/components/Lists.tsx:94
#: src/components/Lists.tsx:95
msgid "Oops, something went wrong!"
msgstr ""
#: src/components/Lists.tsx:173
#: src/components/Lists.tsx:174
#: src/components/StarterPack/ProfileStarterPacks.tsx:332
#: src/components/StarterPack/ProfileStarterPacks.tsx:341
#: src/screens/Settings/AppPasswords.tsx:59
@@ -5834,7 +5835,7 @@ msgstr ""
msgid "Open system log"
msgstr ""
#: src/view/com/util/forms/DropdownButton.tsx:162
#: src/view/com/util/forms/DropdownButton.tsx:167
msgid "Opens {numItems} options"
msgstr ""
@@ -5980,7 +5981,7 @@ msgstr ""
msgid "Our moderators have reviewed reports and decided to disable your access to chats on Bluesky."
msgstr ""
#: src/components/Lists.tsx:190
#: src/components/Lists.tsx:191
#: src/view/screens/NotFound.tsx:47
msgid "Page not found"
msgstr ""
@@ -6408,7 +6409,7 @@ msgid "Press to attempt reconnection"
msgstr ""
#: src/components/Error.tsx:60
#: src/components/Lists.tsx:99
#: src/components/Lists.tsx:100
#: src/screens/Messages/components/MessageListError.tsx:24
#: src/screens/Signup/BackNextButtons.tsx:47
msgid "Press to retry"
@@ -6688,7 +6689,7 @@ msgstr ""
msgid "Reject chat request"
msgstr ""
#: src/screens/Messages/ChatList.tsx:278
#: src/screens/Messages/ChatList.tsx:274
#: src/screens/Messages/Inbox.tsx:212
msgid "Reload conversations"
msgstr ""
@@ -6765,7 +6766,7 @@ msgstr ""
msgid "Remove from your feeds?"
msgstr ""
#: src/view/com/composer/photos/Gallery.tsx:203
#: src/view/com/composer/photos/Gallery.tsx:204
msgid "Remove image"
msgstr ""
@@ -7151,14 +7152,14 @@ msgstr ""
#: src/components/dms/MessageItem.tsx:321
#: src/components/Error.tsx:65
#: src/components/Lists.tsx:110
#: src/components/Lists.tsx:111
#: src/components/moderation/ReportDialog/index.tsx:229
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:55
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:57
#: src/components/StarterPack/ProfileStarterPacks.tsx:346
#: src/screens/Login/LoginForm.tsx:323
#: 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/Inbox.tsx:218
#: src/screens/Onboarding/StepInterests/index.tsx:217
@@ -7538,7 +7539,7 @@ msgstr ""
msgid "Select your preferred notification channels"
msgstr ""
#: src/view/com/util/forms/DropdownButton.tsx:297
#: src/view/com/util/forms/DropdownButton.tsx:302
msgid "Selects option {0} of {numItems}"
msgstr ""
@@ -7901,15 +7902,12 @@ msgstr ""
msgid "Shows the content"
msgstr ""
#: src/screens/Search/SearchResults.tsx:258
msgid "sign in"
msgstr ""
#: src/components/dialogs/Signin.tsx:97
#: src/components/dialogs/Signin.tsx:99
#: src/screens/Login/index.tsx:122
#: src/screens/Login/index.tsx:143
#: 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:69
#: src/view/com/auth/SplashScreen.web.tsx:123
@@ -8051,7 +8049,7 @@ msgstr ""
msgid "Something went wrong, please try again."
msgstr ""
#: src/components/Lists.tsx:174
#: src/components/Lists.tsx:175
msgid "Something went wrong!"
msgstr ""
@@ -8933,7 +8931,7 @@ msgstr ""
msgid "Today"
msgstr ""
#: src/view/com/util/forms/DropdownButton.tsx:258
#: src/view/com/util/forms/DropdownButton.tsx:263
msgid "Toggle dropdown"
msgstr ""
@@ -9517,7 +9515,7 @@ msgstr ""
#: src/screens/Settings/AboutSettings.tsx:126
#: src/screens/Settings/AboutSettings.tsx:155
msgid "Version {appVersion}"
msgid "Version {0}"
msgstr ""
#: 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."
msgstr ""
#: src/components/Lists.tsx:194
#: src/components/Lists.tsx:195
#: src/view/screens/NotFound.tsx:50
msgid "We're sorry! We can't find the page you were looking for."
msgstr ""
@@ -9901,7 +9899,7 @@ msgid "Who can verify?"
msgstr ""
#: src/screens/Home/NoFeedsPinned.tsx:79
#: src/screens/Messages/ChatList.tsx:262
#: src/screens/Messages/ChatList.tsx:258
#: src/screens/Messages/Inbox.tsx:197
msgid "Whoops!"
msgstr ""
@@ -10173,7 +10171,7 @@ msgstr ""
msgid "You have muted this user"
msgstr ""
#: src/screens/Messages/ChatList.tsx:305
#: src/screens/Messages/ChatList.tsx:301
msgid "You have no conversations yet. Start one!"
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."
msgstr ""
#: src/components/Lists.tsx:57
#: src/components/Lists.tsx:58
msgid "You have reached the end"
msgstr ""
+1 -2
View File
@@ -2,8 +2,7 @@ import {init, SessionStrategy} from '@bitdrift/react-native'
import {Statsig} from 'statsig-react-native-expo'
import {initPromise} from '#/lib/statsig/statsig'
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
import {BITDRIFT_API_KEY} from '#/env'
initPromise.then(() => {
let isEnabled = false
+2 -1
View File
@@ -14,9 +14,10 @@ import {
} from '#/logger/types'
import {enabledLogLevels} from '#/logger/util'
import {isNative} from '#/platform/detection'
import {ENV} from '#/env'
const TRANSPORTS: Transport[] = (function configureTransports() {
switch (process.env.NODE_ENV) {
switch (ENV) {
case 'production': {
return [sentryTransport, isNative && bitdriftTransport].filter(
Boolean,
+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 pkgJson from '#/../package.json'
/**
* Examples:
* - `dev`
* - `1.99.0`
*/
const release = process.env.SENTRY_RELEASE || pkgJson.version
/**
* The latest deployed commit hash
*/
const dist = process.env.SENTRY_DIST || 'dev'
import * as env from '#/env'
init({
enabled: !__DEV__ && !!process.env.SENTRY_DSN,
enabled: !env.IS_DEV && !!env.SENTRY_DSN,
autoSessionTracking: false,
dsn: process.env.SENTRY_DSN,
dsn: env.SENTRY_DSN,
debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production
environment: process.env.NODE_ENV,
dist,
release,
environment: env.ENV,
dist: env.BUNDLE_IDENTIFIER,
release: env.RELEASE_VERSION,
ignoreErrors: [
/*
* Unknown internals errors
+26 -30
View File
@@ -55,7 +55,7 @@ type ListItem =
function renderItem({item}: {item: ListItem}) {
switch (item.type) {
case 'INBOX':
return <InboxPreview count={item.count} profiles={item.profiles} />
return <InboxPreview profiles={item.profiles} />
case 'CONVERSATION':
return <ChatListItem convo={item.conversation} />
}
@@ -140,22 +140,24 @@ export function MessagesScreenInner({navigation, route}: Props) {
const leftConvos = useLeftConvos()
const inboxPreviewConvos = useMemo(() => {
const inbox =
inboxData?.pages
.flatMap(page => page.convos)
.filter(
convo =>
!leftConvos.includes(convo.id) &&
!convo.muted &&
convo.unreadCount > 0 &&
convo.members.every(member => member.handle !== 'missing.invalid'),
) ?? []
const inboxAllConvos =
inboxData?.pages
.flatMap(page => page.convos)
.filter(
convo =>
!leftConvos.includes(convo.id) &&
!convo.muted &&
convo.members.every(member => member.handle !== 'missing.invalid'),
) ?? []
const hasInboxConvos = inboxAllConvos?.length > 0
return inbox
.map(x => x.members.find(y => y.did !== currentAccount?.did))
.filter(x => !!x)
}, [inboxData, leftConvos, currentAccount?.did])
const inboxUnreadConvos = inboxAllConvos.filter(
convo => convo.unreadCount > 0,
)
const inboxUnreadConvoMembers = inboxUnreadConvos
.map(x => x.members.find(y => y.did !== currentAccount?.did))
.filter(x => !!x)
const conversations = useMemo(() => {
if (data?.pages) {
@@ -164,15 +166,13 @@ export function MessagesScreenInner({navigation, route}: Props) {
// filter out convos that are actively being left
.filter(convo => !leftConvos.includes(convo.id))
const hasInboxRequests = inboxPreviewConvos?.length > 0
return [
...(hasInboxRequests
...(hasInboxConvos
? [
{
type: 'INBOX' as const,
count: inboxPreviewConvos.length,
profiles: inboxPreviewConvos.slice(0, 3),
count: inboxUnreadConvoMembers.length,
profiles: inboxUnreadConvoMembers.slice(0, 3),
},
]
: []),
@@ -182,7 +182,7 @@ export function MessagesScreenInner({navigation, route}: Props) {
] satisfies ListItem[]
}
return []
}, [data, leftConvos, inboxPreviewConvos])
}, [data, leftConvos, hasInboxConvos, inboxUnreadConvoMembers])
const onRefresh = useCallback(async () => {
setIsPTRing(true)
@@ -231,21 +231,17 @@ export function MessagesScreenInner({navigation, route}: Props) {
// NOTE(APiligrim)
// Show empty state only if there are no conversations at all
const actualConversations = conversations.filter(
const activeConversations = conversations.filter(
item => item.type === 'CONVERSATION',
)
const hasInboxRequests = inboxPreviewConvos?.length > 0
if (actualConversations.length === 0) {
if (activeConversations.length === 0) {
return (
<Layout.Screen>
<Header newChatControl={newChatControl} />
<Layout.Center>
{hasInboxRequests && (
<InboxPreview
count={inboxPreviewConvos.length}
profiles={inboxPreviewConvos}
/>
{!isLoading && hasInboxConvos && (
<InboxPreview profiles={inboxUnreadConvoMembers} />
)}
{isLoading ? (
<ChatListLoadingPlaceholder />
@@ -1,5 +1,5 @@
import {View} from 'react-native'
import {ChatBskyActorDefs} from '@atproto/api'
import {type ChatBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -12,10 +12,8 @@ import {Link} from '#/components/Link'
export function InboxPreview({
profiles,
}: // count,
{
}: {
profiles: ChatBskyActorDefs.ProfileViewBasic[]
count: number
}) {
const {_} = useLingui()
const t = useTheme()
+2 -2
View File
@@ -305,8 +305,8 @@ export function StepFinished() {
<Button
disabled={saving}
key={state.activeStep} // remove focus state on nav
variant="gradient"
color="gradient_sky"
variant="solid"
color="primary"
size="large"
label={_(msg`Complete onboarding and start using your account`)}
onPress={finishOnboarding}>
@@ -15,7 +15,7 @@ import {
TitleText,
} from '#/screens/Onboarding/Layout'
import {
ApiResponseMap,
type ApiResponseMap,
Context,
useInterestsDisplayNames,
} from '#/screens/Onboarding/state'
@@ -235,8 +235,8 @@ export function StepInterests() {
) : (
<Button
disabled={saving || !data}
variant="gradient"
color="gradient_sky"
variant="solid"
color="primary"
size="large"
label={_(msg`Continue to next step`)}
onPress={saveInterests}>
+2 -2
View File
@@ -267,8 +267,8 @@ export function StepProfile() {
<OnboardingControls.Portal>
<View style={[a.gap_md, gtMobile && {flexDirection: 'row-reverse'}]}>
<Button
variant="gradient"
color="gradient_sky"
variant="solid"
color="primary"
size="large"
label={_(msg`Continue to next step`)}
onPress={onContinue}>
+2 -2
View File
@@ -255,7 +255,7 @@ let SearchScreenPostResults = ({
<Trans>
<InlineLinkText
style={[pal.link]}
label={_(msg`sign in`)}
label={_(msg`Sign in`)}
to={'#'}
onPress={showSignIn}>
Sign in
@@ -263,7 +263,7 @@ let SearchScreenPostResults = ({
<Text style={t.atoms.text_contrast_medium}> or </Text>
<InlineLinkText
style={[pal.link]}
label={_(msg`create an account`)}
label={_(msg`Create an account`)}
to={'#'}
onPress={showCreateAccount}>
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 {Statsig} from 'statsig-react-native-expo'
import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info'
import {STATUS_PAGE_URL} from '#/lib/constants'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {isAndroid, isIOS, isNative} from '#/platform/detection'
@@ -23,6 +22,7 @@ import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/i
import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import * as env from '#/env'
import {useDemoMode} from '#/storage/hooks/demo-mode'
import {useDevMode} from '#/storage/hooks/dev-mode'
import {OTAInfo} from './components/OTAInfo'
@@ -123,7 +123,7 @@ export function AboutSettingsScreen({}: Props) {
</SettingsList.PressableItem>
)}
<SettingsList.PressableItem
label={_(msg`Version ${appVersion}`)}
label={_(msg`Version ${env.APP_VERSION}`)}
accessibilityHint={_(msg`Copies build version to clipboard`)}
onLongPress={() => {
const newDevModeEnabled = !devModeEnabled
@@ -146,15 +146,15 @@ export function AboutSettingsScreen({}: Props) {
}}
onPress={() => {
setStringAsync(
`Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`,
`Build version: ${env.APP_VERSION}; Bundle info: ${env.APP_METADATA}; Bundle date: ${env.BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`,
)
Toast.show(_(msg`Copied build version to clipboard`))
}}>
<SettingsList.ItemIcon icon={WrenchIcon} />
<SettingsList.ItemText>
<Trans>Version {appVersion}</Trans>
<Trans>Version {env.APP_VERSION}</Trans>
</SettingsList.ItemText>
<SettingsList.BadgeText>{bundleInfo}</SettingsList.BadgeText>
<SettingsList.BadgeText>{env.APP_METADATA}</SettingsList.BadgeText>
</SettingsList.PressableItem>
{devModeEnabled && (
<>
@@ -3,20 +3,20 @@ import {Alert, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {IS_INTERNAL} from '#/lib/app-info'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {isAndroid} from '#/platform/detection'
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types'
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import {IS_INTERNAL} from '#/env'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
export function AppIconSettingsScreen({}: Props) {
+1 -1
View File
@@ -8,7 +8,6 @@ import Animated, {
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {IS_INTERNAL} from '#/lib/app-info'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
@@ -26,6 +25,7 @@ import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/T
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import {IS_INTERNAL} from '#/env'
import * as SettingsList from './components/SettingsList'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
+1 -1
View File
@@ -9,7 +9,6 @@ import {useNavigation} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useActorStatus} from '#/lib/actor-status'
import {IS_INTERNAL} from '#/lib/app-info'
import {HELP_DESK_URL} from '#/lib/constants'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
@@ -66,6 +65,7 @@ import {
shouldShowVerificationCheckButton,
VerificationCheckButton,
} from '#/components/verification/VerificationCheckButton'
import {IS_INTERNAL} from '#/env'
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
@@ -124,7 +124,7 @@ export function LinkItem({
contentContainerStyle,
chevronColor,
...props
}: LinkProps & {
}: Omit<LinkProps, Button.UninheritableButtonProps> & {
contentContainerStyle?: StyleProp<ViewStyle>
destructive?: boolean
chevronColor?: string
@@ -132,7 +132,7 @@ export function LinkItem({
const t = useTheme()
return (
<Link color="secondary" {...props}>
<Link {...props}>
{args => (
<Item
destructive={destructive}
@@ -154,7 +154,7 @@ export function PressableItem({
contentContainerStyle,
hoverStyle,
...props
}: Button.ButtonProps & {
}: Omit<Button.ButtonProps, Button.UninheritableButtonProps> & {
contentContainerStyle?: StyleProp<ViewStyle>
destructive?: boolean
}) {
+9
View File
@@ -43,6 +43,9 @@ export interface ChangePasswordModal {
name: 'change-password'
}
/**
* @deprecated DO NOT ADD NEW MODALS
*/
export type Modal =
// Account
| DeleteAccountModal
@@ -125,10 +128,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
}
/**
* @deprecated use the dialog system from `#/components/Dialog.tsx`
*/
export function useModals() {
return React.useContext(ModalContext)
}
/**
* @deprecated use the dialog system from `#/components/Dialog.tsx`
*/
export function useModalControls() {
return React.useContext(ModalControlContext)
}
+3 -1
View File
@@ -1,3 +1,5 @@
import {CHAT_PROXY_DID} from '#/env'
export const DM_SERVICE_HEADERS = {
'atproto-proxy': 'did:web:api.bsky.chat#bsky_chat',
'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`,
}
+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 {Statsig} from 'statsig-react-native-expo'
import {IS_INTERNAL} from '#/lib/app-info'
import {Schema} from '../persisted'
import {Action, State} from './reducer'
import {SessionAccount} from './types'
import {IS_INTERNAL} from '#/env'
import {type Schema} from '../persisted'
import {type Action, type State} from './reducer'
import {type SessionAccount} from './types'
type Reducer = (state: State, action: Action) => State
+20
View File
@@ -369,3 +369,23 @@ input[type='range'][orient='vertical']::-moz-range-thumb {
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 {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {type PostAction} from '../state/composer'
import {EditImageDialog} from './EditImageDialog'
import {ImageAltTextDialog} from './ImageAltTextDialog'
@@ -227,8 +228,12 @@ const GalleryItem = ({
}}
accessible={true}
accessibilityIgnoresInvertColors
cachePolicy="none"
autoplay={false}
/>
<MediaInsetBorder />
<ImageAltTextDialog
control={altTextControl}
image={image}
+9
View File
@@ -56,6 +56,9 @@ interface Props extends React.ComponentProps<typeof TouchableOpacity> {
onBeforePress?: () => void
}
/**
* @deprecated use Link from `#/components/Link.tsx` instead
*/
export const Link = memo(function Link({
testID,
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({
testID,
type = 'md',
@@ -301,6 +307,9 @@ interface TextLinkOnWebOnlyProps extends TextProps {
onPointerEnter?: () => void
anchorNoUnderline?: boolean
}
/**
* @deprecated use WebOnlyInlineLinkText from `#/components/Link.tsx` instead
*/
export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
testID,
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 {AccessibilityInfo, View} from 'react-native'
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 {toast} from '#/components/Toast'
import {type ToastType} from '#/components/Toast/types'
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(
message: string,
type: ToastType | LegacyToastType = 'default',
): void {
if (process.env.NODE_ENV === 'test') {
return
}
AccessibilityInfo.announceForAccessibility(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>
)
const convertedType = convertLegacyToastType(type)
toast.show({
type: convertedType,
content: message,
a11yLabel: message,
})
}
-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.
*
* @deprecated
* @deprecated use `Layout.Header` from `#/components/Layout.tsx`
*/
export function ViewHeader({
title,
+10 -8
View File
@@ -1,16 +1,16 @@
import React from 'react'
import {
ActivityIndicator,
GestureResponderEvent,
NativeSyntheticEvent,
NativeTouchEvent,
type GestureResponderEvent,
type NativeSyntheticEvent,
type NativeTouchEvent,
Pressable,
PressableStateCallbackType,
StyleProp,
type PressableStateCallbackType,
type StyleProp,
StyleSheet,
TextStyle,
type TextStyle,
View,
ViewStyle,
type ViewStyle,
} from 'react-native'
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({
type = 'primary',
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 {
Dimensions,
GestureResponderEvent,
Insets,
StyleProp,
type GestureResponderEvent,
type Insets,
type StyleProp,
StyleSheet,
TouchableOpacity,
TouchableWithoutFeedback,
useWindowDimensions,
View,
ViewStyle,
type ViewStyle,
} from 'react-native'
import Animated, {FadeIn, FadeInDown, FadeInUp} from 'react-native-reanimated'
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 {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import type React from 'react'
import {HITSLOP_10} from '#/lib/constants'
import {usePalette} from '#/lib/hooks/usePalette'
@@ -26,7 +28,7 @@ import {isWeb} from '#/platform/detection'
import {native} from '#/alf'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {Text} from '../text/Text'
import {Button, ButtonType} from './Button'
import {Button, type ButtonType} from './Button'
const ESTIMATED_BTN_HEIGHT = 50
const ESTIMATED_SEP_HEIGHT = 16
@@ -70,6 +72,9 @@ interface DropdownButtonProps {
accessibilityHint?: string
}
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export function DropdownButton({
testID,
type = 'bare',
+34 -4
View File
@@ -1,9 +1,15 @@
import React from 'react'
import {Platform, Pressable, StyleSheet, View, ViewStyle} from 'react-native'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {
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 * 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 {useTheme} from '#/lib/ThemeContext'
@@ -12,8 +18,14 @@ import {Portal} from '#/components/Portal'
// Custom Dropdown Menu Components
// ==
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuRoot = DropdownMenu.Root
// export const DropdownMenuTrigger = DropdownMenu.Trigger
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuContent = DropdownMenu.Content
type TriggerProps = Omit<
@@ -25,6 +37,9 @@ type TriggerProps = Omit<
accessibilityLabel?: string
accessibilityHint?: string
}>
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuTrigger = DropdownMenu.create(
(props: TriggerProps) => {
const theme = useTheme()
@@ -59,6 +74,9 @@ export const DropdownMenuTrigger = DropdownMenu.create(
)
type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']>
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuItem = DropdownMenu.create(
(props: ItemProps & {testID?: string}) => {
const theme = useTheme()
@@ -84,6 +102,9 @@ export const DropdownMenuItem = DropdownMenu.create(
)
type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']>
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuItemTitle = DropdownMenu.create(
(props: TitleProps) => {
const pal = usePalette('default')
@@ -98,11 +119,17 @@ export const DropdownMenuItemTitle = DropdownMenu.create(
)
type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']>
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => {
return <DropdownMenu.ItemIcon {...props} />
}, 'ItemIcon')
type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']>
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export const DropdownMenuSeparator = DropdownMenu.create(
(props: SeparatorProps) => {
const pal = usePalette('default')
@@ -142,11 +169,14 @@ type Props = {
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
* Radix UI primitives under the hood
* @prop {DropdownItem[]} items - An array of dropdown items
* @prop {React.ReactNode} children - A custom dropdown trigger
*
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export function NativeDropdown({
items,
@@ -63,6 +63,9 @@ type Props = {
triggerStyle?: ViewStyle
}
/**
* @deprecated use Menu from `#/components/Menu.tsx` instead
*/
export function NativeDropdown({
items,
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 {colors} from '#/lib/styles'
import {useTheme} from '#/lib/ThemeContext'
import {TypographyVariant} from '#/lib/ThemeContext'
import {type TypographyVariant} from '#/lib/ThemeContext'
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({
testID,
type = 'default-light',
+4 -4
View File
@@ -1,16 +1,16 @@
import React from 'react'
import {StyleSheet, TextProps} from 'react-native'
import {StyleSheet, type TextProps} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import {lh, s} from '#/lib/styles'
import {TypographyVariant, useTheme} from '#/lib/ThemeContext'
import {type TypographyVariant, useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {isIOS, isWeb} from '#/platform/detection'
import {applyFonts, useAlf} from '#/alf'
import {
childHasEmoji,
renderChildrenWithEmoji,
StringChild,
type StringChild,
} from '#/alf/typography'
export type CustomTextProps = Omit<TextProps, 'children'> & {
@@ -32,7 +32,7 @@ export type CustomTextProps = Omit<TextProps, 'children'> & {
export {Text_DEPRECATED as Text}
/**
* @deprecated use Text from Typography instead.
* @deprecated use Text from `#/components/Typography.tsx` instead
*/
function Text_DEPRECATED({
type = 'md',
+43 -167
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a} from '#/alf'
@@ -7,7 +6,6 @@ import {
type ButtonColor,
ButtonIcon,
ButtonText,
type ButtonVariant,
} from '#/components/Button'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
@@ -27,258 +25,136 @@ export function Buttons() {
'negative_secondary',
].map(color => (
<View key={color} style={[a.gap_md, a.align_start]}>
{['solid', 'outline', 'ghost'].map(variant => (
<React.Fragment key={variant}>
<Button
variant={variant as ButtonVariant}
color={color as ButtonColor}
size="large"
label="Click here">
<ButtonText>Button</ButtonText>
</Button>
<Button
disabled
variant={variant as ButtonVariant}
color={color as ButtonColor}
size="large"
label="Click here">
<ButtonText>Button</ButtonText>
</Button>
</React.Fragment>
))}
<Button
color={color as ButtonColor}
size="large"
label="Click here">
<ButtonText>Button</ButtonText>
</Button>
<Button
disabled
color={color as ButtonColor}
size="large"
label="Click here">
<ButtonText>Button</ButtonText>
</Button>
</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 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>
</Button>
<Button variant="solid" color="primary" size="large" label="Link out">
<Button color="primary" size="large" label="Link out">
<ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" />
</Button>
<Button variant="solid" color="primary" size="small" label="Link out">
<Button color="primary" size="small" label="Link out">
<ButtonText>Button</ButtonText>
</Button>
<Button variant="solid" color="primary" size="small" label="Link out">
<Button color="primary" size="small" label="Link out">
<ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" />
</Button>
<Button variant="solid" color="primary" size="tiny" label="Link out">
<Button color="primary" size="tiny" label="Link out">
<ButtonIcon icon={Globe} position="left" />
<ButtonText>Button</ButtonText>
</Button>
</View>
<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>
</Button>
<Button variant="solid" color="primary" size="large" label="Link out">
<Button color="primary" size="large" label="Link out">
<ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" />
</Button>
<Button variant="solid" color="primary" size="large" label="Link out">
<Button color="primary" size="large" label="Link out">
<ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" size="lg" />
</Button>
<Button
variant="solid"
color="primary"
size="large"
shape="round"
label="Link out">
<Button color="primary" size="large" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="solid"
color="primary"
size="large"
shape="round"
label="Link out">
<Button color="primary" size="large" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} size="lg" />
</Button>
</View>
<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>
</Button>
<Button variant="solid" color="primary" size="small" label="Link out">
<Button color="primary" size="small" label="Link out">
<ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" />
</Button>
<Button
variant="solid"
color="primary"
size="small"
shape="round"
label="Link out">
<Button color="primary" size="small" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="solid"
color="primary"
size="small"
shape="round"
label="Link out">
<Button color="primary" size="small" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} size="lg" />
</Button>
</View>
<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>
</Button>
<Button variant="solid" color="primary" size="tiny" label="Link out">
<Button color="primary" size="tiny" label="Link out">
<ButtonText>Button</ButtonText>
<ButtonIcon icon={Globe} position="right" />
</Button>
<Button
variant="solid"
color="primary"
size="tiny"
shape="round"
label="Link out">
<Button color="primary" size="tiny" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="solid"
color="primary"
size="tiny"
shape="round"
label="Link out">
<Button color="primary" size="tiny" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} size="md" />
</Button>
</View>
<View style={[a.flex_row, a.gap_md, a.align_center]}>
<Button
variant="solid"
color="primary"
size="large"
shape="round"
label="Link out">
<Button color="primary" size="large" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="gradient"
color="gradient_sunset"
size="small"
shape="round"
label="Link out">
<Button color="primary" size="small" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="gradient"
color="gradient_sunset"
size="tiny"
shape="round"
label="Link out">
<Button color="primary" size="tiny" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="outline"
color="primary"
size="large"
shape="round"
label="Link out">
<Button color="primary" size="large" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="ghost"
color="primary"
size="small"
shape="round"
label="Link out">
<Button color="primary" size="small" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="ghost"
color="primary"
size="tiny"
shape="round"
label="Link out">
<Button color="primary" size="tiny" shape="round" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
</View>
<View style={[a.flex_row, a.gap_md, a.align_start]}>
<Button
variant="solid"
color="primary"
size="large"
shape="square"
label="Link out">
<Button color="primary" size="large" shape="square" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="gradient"
color="gradient_sunset"
size="small"
shape="square"
label="Link out">
<Button color="primary" size="small" shape="square" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="gradient"
color="gradient_sunset"
size="tiny"
shape="square"
label="Link out">
<Button color="primary" size="tiny" shape="square" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="outline"
color="primary"
size="large"
shape="square"
label="Link out">
<Button color="primary" size="large" shape="square" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="ghost"
color="primary"
size="small"
shape="square"
label="Link out">
<Button color="primary" size="small" shape="square" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
<Button
variant="ghost"
color="primary"
size="tiny"
shape="square"
label="Link out">
<Button color="primary" size="tiny" shape="square" label="Link out">
<ButtonIcon icon={ChevronLeft} />
</Button>
</View>
+3 -3
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {TextInput, View} from 'react-native'
import {type TextInput, View} from 'react-native'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -216,8 +216,8 @@ export function Forms() {
</View>
<Button
variant="gradient"
color="gradient_nordic"
variant="solid"
color="primary"
size="small"
label="Reset all toggles"
onPress={() => {
+101 -87
View File
@@ -1,65 +1,11 @@
import {Pressable, View} from 'react-native'
import * as Toast from '#/view/com/util/Toast'
import {
getToastTypeStyles,
TOAST_TYPE_TO_ICON,
type ToastType,
} from '#/view/com/util/Toast.style'
import {atoms as a, useTheme} from '#/alf'
import {H1, Text} from '#/components/Typography'
function ToastPreview({message, type}: {message: string; type: ToastType}) {
const t = useTheme()
const toastStyles = getToastTypeStyles(t)
const colors = toastStyles[type as keyof typeof toastStyles]
const IconComponent =
TOAST_TYPE_TO_ICON[type as keyof typeof TOAST_TYPE_TO_ICON]
return (
<Pressable
accessibilityRole="button"
onPress={() => Toast.show(message, type)}
style={[
{backgroundColor: colors.backgroundColor},
a.shadow_sm,
{borderColor: colors.borderColor},
a.rounded_sm,
a.border,
a.px_sm,
a.py_sm,
a.flex_row,
a.gap_sm,
a.align_center,
]}>
<View
style={[
a.flex_shrink_0,
a.rounded_full,
{width: 24, height: 24},
a.align_center,
a.justify_center,
{
backgroundColor: colors.backgroundColor,
},
]}>
<IconComponent fill={colors.iconColor} size="xs" />
</View>
<View style={[a.flex_1]}>
<Text
style={[
a.text_sm,
a.font_bold,
a.leading_snug,
{color: colors.textColor},
]}
emoji>
{message}
</Text>
</View>
</Pressable>
)
}
import {show as deprecatedShow} from '#/view/com/util/Toast'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {toast} from '#/components/Toast'
import {Toast} from '#/components/Toast/Toast'
import {H1} from '#/components/Typography'
export function Toasts() {
return (
@@ -67,35 +13,103 @@ export function Toasts() {
<H1>Toast Examples</H1>
<View style={[a.gap_md]}>
<View style={[a.gap_xs]}>
<ToastPreview message="Default Toast" type="default" />
</View>
<View style={[a.gap_xs]}>
<ToastPreview
message="Operation completed successfully!"
type="success"
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'default',
content: 'Default toast',
a11yLabel: 'Default toast',
})
}>
<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]}>
<ToastPreview message="Something went wrong!" type="error" />
</View>
<View style={[a.gap_xs]}>
<ToastPreview message="Please check your input" type="warning" />
</View>
<View style={[a.gap_xs]}>
<ToastPreview message="Here's some helpful information" type="info" />
</View>
<View style={[a.gap_xs]}>
<ToastPreview
message="This is a longer message to test how the toast handles multiple lines of text content."
type="info"
/>
</View>
<Button
label="Deprecated toast example"
onPress={() =>
deprecatedShow(
'This is a deprecated toast example',
'exclamation-circle',
)
}
size="large"
variant="solid"
color="secondary">
<ButtonText>Deprecated toast example</ButtonText>
</Button>
</View>
</View>
)
+2 -8
View File
@@ -52,7 +52,6 @@ function StorybookInner() {
<>
<View style={[a.flex_row, a.align_start, a.gap_md]}>
<Button
variant="outline"
color="primary"
size="small"
label='Set theme to "system"'
@@ -60,7 +59,6 @@ function StorybookInner() {
<ButtonText>System</ButtonText>
</Button>
<Button
variant="solid"
color="secondary"
size="small"
label='Set theme to "light"'
@@ -68,7 +66,6 @@ function StorybookInner() {
<ButtonText>Light</ButtonText>
</Button>
<Button
variant="solid"
color="secondary"
size="small"
label='Set theme to "dim"'
@@ -79,7 +76,6 @@ function StorybookInner() {
<ButtonText>Dim</ButtonText>
</Button>
<Button
variant="solid"
color="secondary"
size="small"
label='Set theme to "dark"'
@@ -91,8 +87,9 @@ function StorybookInner() {
</Button>
</View>
<Toasts />
<Button
variant="solid"
color="primary"
size="small"
onPress={() => navigation.navigate('SharedPreferencesTester')}
@@ -123,11 +120,9 @@ function StorybookInner() {
<Breakpoints />
<Dialogs />
<Admonitions />
<Toasts />
<Settings />
<Button
variant="solid"
color="primary"
size="large"
label="Switch to Contained List"
@@ -138,7 +133,6 @@ function StorybookInner() {
) : (
<>
<Button
variant="solid"
color="primary"
size="large"
label="Switch to Storybook"
+1 -1
View File
@@ -53,7 +53,7 @@ module.exports = async function (env, argv) {
project: 'app',
authToken: process.env.SENTRY_AUTH_TOKEN,
release: {
// env is undefined for Render.com builds, fall back
// fallback needed for Render.com deployments
name: process.env.SENTRY_RELEASE || version,
dist: process.env.SENTRY_DIST,
},