Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79305adf3d |
+6
-30
@@ -1,32 +1,8 @@
|
||||
# The env the app is running in e.g. development, testflight, production
|
||||
EXPO_PUBLIC_ENV=development
|
||||
# Copy this to `.env` and `.env.test` files
|
||||
|
||||
# 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
|
||||
BITDRIFT_API_KEY=
|
||||
SENTRY_AUTH_TOKEN=
|
||||
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=
|
||||
EXPO_PUBLIC_LOG_DEBUG=
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
|
||||
EXPO_PUBLIC_BUNDLE_DATE=0
|
||||
|
||||
@@ -43,11 +43,12 @@ jobs:
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
|
||||
- name: Env
|
||||
id: env
|
||||
- name: Set outputs
|
||||
id: vars
|
||||
run: |
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
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
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
@@ -61,8 +62,8 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
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 }}
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.vars.outputs.sha_short }}
|
||||
SENTRY_DIST=${{ steps.vars.outputs.SENTRY_DIST }}
|
||||
SENTRY_RELEASE=${{ steps.vars.outputs.SENTRY_RELEASE }}
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
|
||||
|
||||
@@ -3,7 +3,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- echoprom_fix
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
|
||||
@@ -13,7 +13,6 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
name: Build and Submit Android
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -62,31 +61,23 @@ 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: Env
|
||||
id: env
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .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_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "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_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
|
||||
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
|
||||
|
||||
- name: ✍️ Rename Testflight bundle
|
||||
if: ${{ inputs.profile != 'production' }}
|
||||
@@ -148,14 +139,7 @@ jobs:
|
||||
|
||||
- name: 🏗️ Build Production APK
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
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
|
||||
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
|
||||
|
||||
- name: 🚀 Upload Production APK Artifact
|
||||
id: upload-artifact-production-apk
|
||||
|
||||
@@ -13,7 +13,6 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
@@ -53,7 +52,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '16.4'
|
||||
xcode-version: '16.2'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
@@ -75,30 +74,22 @@ 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_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_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "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_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
|
||||
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
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
|
||||
@@ -20,7 +20,6 @@ on:
|
||||
|
||||
jobs:
|
||||
bundleDeploy:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
name: Bundle and Deploy EAS Update
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
@@ -101,31 +100,25 @@ jobs:
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
# eas.json not used here, set EXPO_PUBLIC_ENV
|
||||
- name: Env
|
||||
id: env
|
||||
- name: ✏️ Write environment variables
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .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_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "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_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
|
||||
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
|
||||
|
||||
- name: 📦 Package Bundle and 🚀 Deploy
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
@@ -157,7 +150,7 @@ jobs:
|
||||
needs: [bundleDeploy]
|
||||
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||
# available here
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app' }}
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
@@ -211,30 +204,16 @@ jobs:
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build
|
||||
|
||||
# EXPO_PUBLIC_ENV is handled in eas.json
|
||||
- name: Env
|
||||
id: env
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .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_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
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
|
||||
run: 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
|
||||
@@ -260,7 +239,7 @@ jobs:
|
||||
needs: [bundleDeploy]
|
||||
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||
# available here
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app'}}
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
|
||||
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
@@ -302,31 +281,17 @@ jobs:
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build
|
||||
|
||||
# EXPO_PUBLIC_ENV is handled in eas.json
|
||||
- name: Env
|
||||
id: env
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .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_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
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
|
||||
run: 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
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
---
|
||||
name: PR Comment Trigger
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
# Permissiosn to make comments in the pull request
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
handle-comment:
|
||||
if: github.event.issue.pull_request
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should-deploy: ${{ steps.check-org.outputs.result }}
|
||||
|
||||
steps:
|
||||
- name: Check if bot is mentioned
|
||||
id: check-mention
|
||||
env:
|
||||
COMMENT: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
if [[ "$COMMENT" == *"@github-actions"* ]] || \
|
||||
[[ "$COMMENT" == *"github-actions[bot]"* ]]; then
|
||||
bot_mentioned=true
|
||||
else
|
||||
bot_mentioned=false
|
||||
fi
|
||||
|
||||
|
||||
if [[ "${{ github.event.comment.body }}" == *"ota"* ]]; then
|
||||
has_ota=true
|
||||
else
|
||||
has_ota=false
|
||||
fi
|
||||
|
||||
|
||||
if [[ "$bot_mentioned" == "true" ]] && [[ "$has_ota" == "true" ]]; then
|
||||
echo "mentioned=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "mentioned=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check organization membership
|
||||
if: steps.check-mention.outputs.mentioned == 'true'
|
||||
id: check-org
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: context.payload.comment.user.login
|
||||
});
|
||||
|
||||
const hasAccess = ['admin', 'write'].includes(perm.permission);
|
||||
console.log(`User has ${perm.permission} access`);
|
||||
|
||||
return hasAccess;
|
||||
} catch(error) {
|
||||
console.log('User has no repository access');
|
||||
return false;
|
||||
}
|
||||
|
||||
bundle-deploy:
|
||||
name: Bundle and Deploy EAS Update
|
||||
runs-on: ubuntu-latest
|
||||
needs: [handle-comment]
|
||||
if: needs.handle-comment.outputs.should-deploy == 'true'
|
||||
|
||||
steps:
|
||||
- name: Get PR HEAD SHA
|
||||
id: pr-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: ${{ github.event.issue.number }}
|
||||
});
|
||||
|
||||
console.log(`PR HEAD SHA: ${pr.data.head.sha}`);
|
||||
console.log(`PR HEAD REF: ${pr.data.head.ref}`);
|
||||
|
||||
core.setOutput('head-sha', pr.data.head.sha);
|
||||
core.setOutput('head-ref', pr.data.head.ref);
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
number: ${{ github.event.issue.number }}
|
||||
message: |
|
||||
An OTA deployment has been requested and is now running for `${{ steps.pr-info.outputs.head-sha }}`.
|
||||
|
||||
[Here is some music to listen to while you wait...](https://www.youtube.com/watch?v=VBlFHuCzPgY)
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.pr-info.outputs.head-sha }}
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Lint check
|
||||
run: yarn lint
|
||||
|
||||
- name: Lint lockfile
|
||||
run: yarn lockfile-lint
|
||||
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build 2>&1 | tee i18n.log
|
||||
|
||||
- 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: Type check
|
||||
run: yarn typecheck
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup Expo
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: Env
|
||||
id: env
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_ENV=testflight" >> .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ Create Bundle
|
||||
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
|
||||
env:
|
||||
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
|
||||
CHANNEL_NAME: pull-request-${{ github.event.issue.number }}
|
||||
RUNTIME_VERSION:
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
number: ${{ github.event.issue.number }}
|
||||
message: |
|
||||
Your requested OTA deployment was successful! You may now apply it by opening the deep link below in your browser:
|
||||
|
||||
`bluesky://intent/apply-ota?channel=pull-request-${{ github.event.issue.number }}`
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: failure()
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
number: ${{ github.event.issue.number }}
|
||||
message: |
|
||||
Your requested OTA deployment was unsuccessful. See action logs for more details.
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
@@ -50,7 +50,6 @@ jobs:
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git merge --no-edit ${{ github.head_ref }}
|
||||
yarn install
|
||||
yarn intl:build
|
||||
|
||||
- name: 🔦 Generate stats file for PR
|
||||
run: |
|
||||
@@ -74,7 +73,6 @@ jobs:
|
||||
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
|
||||
run: |
|
||||
yarn install
|
||||
yarn intl:build
|
||||
yarn generate-webpack-stats-file
|
||||
mv stats.json stats-base.json
|
||||
|
||||
@@ -145,3 +143,4 @@ jobs:
|
||||
with:
|
||||
header: fingerprint-diff
|
||||
delete: true
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
name: Sync to internal repo
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
steps:
|
||||
- name: Checkout public repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ vars.SYNC_INTERNAL_APP_ID }}
|
||||
private-key: ${{ secrets.SYNC_INTERNAL_PK }}
|
||||
repositories: social-app-internal
|
||||
- name: Push to internal repo
|
||||
env:
|
||||
TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email "test@users.noreply.github.com"
|
||||
git config --unset-all http.https://github.com/.extraheader
|
||||
git remote add internal https://x-access-token:${TOKEN}@github.com/bluesky-social/social-app-internal.git
|
||||
git push internal main --force
|
||||
@@ -58,7 +58,6 @@ buck-out/
|
||||
# Ruby / CocoaPods
|
||||
/ios/Pods/
|
||||
/vendor/bundle/
|
||||
Gemfile.lock
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
+14
-17
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.5-bullseye AS build-env
|
||||
FROM golang:1.23-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
|
||||
# If not set by GitHub workflows, we're probably in Render
|
||||
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-$RENDER_GIT_COMMIT}
|
||||
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
|
||||
|
||||
#
|
||||
# Sentry
|
||||
#
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown}
|
||||
ARG EXPO_PUBLIC_SENTRY_DSN
|
||||
ENV EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN
|
||||
# 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
|
||||
|
||||
#
|
||||
# Copy everything into the container
|
||||
@@ -60,16 +60,13 @@ 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 && \
|
||||
SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$EXPO_PUBLIC_RELEASE_VERSION SENTRY_DIST=$EXPO_PUBLIC_BUNDLE_IDENTIFIER yarn build-web
|
||||
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
|
||||
|
||||
# DEBUG
|
||||
RUN find ./bskyweb/static && find ./web-build/static
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.5-bullseye AS build-env
|
||||
FROM golang:1.23-bullseye AS build-env
|
||||
|
||||
WORKDIR /usr/src/social-app
|
||||
|
||||
|
||||
@@ -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)), which are 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)), code for which is 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,7 +27,6 @@ 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:**
|
||||
@@ -60,7 +59,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 CC'd 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 CCed to the entire team and we'll respond promptly.
|
||||
|
||||
## Are you a developer interested in building on atproto?
|
||||
|
||||
|
||||
@@ -28,9 +28,6 @@ appId: xyz.blueskyweb.app
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn:
|
||||
text: "Not Now"
|
||||
optional: true
|
||||
- inputText: "e2e-test"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
|
||||
@@ -23,7 +23,4 @@ appId: xyz.blueskyweb.app
|
||||
id: "loginPasswordInput"
|
||||
- inputText: "hunter2"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
text: "Not Now"
|
||||
optional: true
|
||||
- assertVisible: "Following"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, {type ReactNode} from 'react'
|
||||
import {FlatList, Modal, ScrollView, TextInput, View} from 'react-native'
|
||||
import React, {ReactNode} from 'react'
|
||||
import {View, ScrollView, Modal, FlatList, TextInput} from 'react-native'
|
||||
|
||||
const BottomSheetModalContext = React.createContext(null)
|
||||
BottomSheetModalContext.displayName = 'BottomSheetModalContext'
|
||||
|
||||
const BottomSheetModalProvider = (props: any) => {
|
||||
return <BottomSheetModalContext.Provider {...props} value={{}} />
|
||||
@@ -48,13 +47,13 @@ export {useBottomSheetInternal}
|
||||
export {useBottomSheetDynamicSnapPoints}
|
||||
|
||||
export {
|
||||
BottomSheetModalProvider,
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetFlatList,
|
||||
BottomSheetFooter,
|
||||
BottomSheetHandle,
|
||||
BottomSheetModal,
|
||||
BottomSheetModalProvider,
|
||||
BottomSheetFooter,
|
||||
BottomSheetScrollView,
|
||||
BottomSheetFlatList,
|
||||
BottomSheetTextInput,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {type IsValidHandle, validateServiceHandle} from '#/lib/strings/handles'
|
||||
import {IsValidHandle, validateServiceHandle} from '#/lib/strings/handles'
|
||||
|
||||
describe('handle validation', () => {
|
||||
const valid = [
|
||||
@@ -18,17 +18,17 @@ describe('handle validation', () => {
|
||||
})
|
||||
|
||||
const invalid = [
|
||||
['al', 'bsky.social', 'frontLengthNotTooShort'],
|
||||
['al', 'bsky.social', 'frontLength'],
|
||||
['-alice', 'bsky.social', 'hyphenStartOrEnd'],
|
||||
['alice-', 'bsky.social', 'hyphenStartOrEnd'],
|
||||
['%%%', 'bsky.social', 'handleChars'],
|
||||
['1234567890123456789', 'bsky.social', 'frontLengthNotTooLong'],
|
||||
['1234567890123456789', 'bsky.social', 'frontLength'],
|
||||
[
|
||||
'1234567890123456789',
|
||||
'my-custom-pds-with-long-name.social',
|
||||
'frontLengthNotTooLong',
|
||||
'frontLength',
|
||||
],
|
||||
['al', 'my-custom-pds-with-long-name.social', 'frontLengthNotTooShort'],
|
||||
['al', 'my-custom-pds-with-long-name.social', 'frontLength'],
|
||||
['a'.repeat(300), 'toolong.com', 'totalLength'],
|
||||
] satisfies [string, string, keyof IsValidHandle][]
|
||||
it.each(invalid)(
|
||||
|
||||
@@ -32,7 +32,6 @@ describe('getMentionAt', () => {
|
||||
['@alice hello', 7, undefined],
|
||||
['alice@alice', 0, undefined],
|
||||
['alice@alice', 6, undefined],
|
||||
['hello @alice-com goodbye', 8, 'alice-com'],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
@@ -73,7 +72,6 @@ describe('insertMentionAt', () => {
|
||||
['@alice hello', 7, '@alice hello'],
|
||||
['alice@alice', 0, 'alice@alice'],
|
||||
['alice@alice', 6, 'alice@alice'],
|
||||
['hello @alice-com goodbye', 10, 'hello @alice.com goodbye'],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
|
||||
+10
-7
@@ -26,7 +26,12 @@ module.exports = function (_config) {
|
||||
...(IS_DEV || IS_TESTFLIGHT ? [] : []),
|
||||
]
|
||||
|
||||
const UPDATES_ENABLED = IS_TESTFLIGHT || IS_PRODUCTION
|
||||
const UPDATES_CHANNEL = IS_TESTFLIGHT
|
||||
? 'testflight'
|
||||
: IS_PRODUCTION
|
||||
? 'production'
|
||||
: undefined
|
||||
const UPDATES_ENABLED = !!UPDATES_CHANNEL
|
||||
|
||||
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
|
||||
|
||||
@@ -185,6 +190,7 @@ module.exports = function (_config) {
|
||||
}
|
||||
: undefined,
|
||||
checkAutomatically: 'NEVER',
|
||||
channel: UPDATES_CHANNEL,
|
||||
},
|
||||
plugins: [
|
||||
'expo-video',
|
||||
@@ -234,9 +240,8 @@ module.exports = function (_config) {
|
||||
],
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withGradleJVMHeapSizeIncrease.js',
|
||||
'./plugins/withAndroidManifestLargeHeapPlugin.js',
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidManifestIntentQueriesPlugin.js',
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
'./plugins/withAndroidDayNightThemePlugin.js',
|
||||
'./plugins/withAndroidNoJitpackPlugin.js',
|
||||
@@ -251,12 +256,10 @@ module.exports = function (_config) {
|
||||
// Android only
|
||||
'./assets/fonts/inter/Inter-Regular.otf',
|
||||
'./assets/fonts/inter/Inter-Italic.otf',
|
||||
'./assets/fonts/inter/Inter-Medium.otf',
|
||||
'./assets/fonts/inter/Inter-MediumItalic.otf',
|
||||
'./assets/fonts/inter/Inter-SemiBold.otf',
|
||||
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-Bold.otf',
|
||||
'./assets/fonts/inter/Inter-BoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-ExtraBold.otf',
|
||||
'./assets/fonts/inter/Inter-ExtraBoldItalic.otf',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 334 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 250 B |
@@ -160,7 +160,7 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
|
||||
richText.push(
|
||||
<Link
|
||||
key={counter}
|
||||
href={`/hashtag/${segment.tag.tag}`}
|
||||
href={`/tag/${segment.tag.tag}`}
|
||||
className="text-blue-500 hover:underline">
|
||||
{segment.text}
|
||||
</Link>,
|
||||
|
||||
@@ -606,10 +606,8 @@ type IPCCRequest struct {
|
||||
type IPCCResponse struct {
|
||||
CC string `json:"countryCode"`
|
||||
AgeRestrictedGeo bool `json:"isAgeRestrictedGeo,omitempty"`
|
||||
AgeBlockedGeo bool `json:"isAgeBlockedGeo,omitempty"`
|
||||
}
|
||||
|
||||
// This product includes GeoLite2 Data created by MaxMind, available from https://www.maxmind.com.
|
||||
func (srv *Server) WebIpCC(c echo.Context) error {
|
||||
realIP := c.RealIP()
|
||||
addr, err := netip.ParseAddr(realIP)
|
||||
|
||||
@@ -46,13 +46,6 @@ func run(args []string) {
|
||||
Value: ":8100",
|
||||
EnvVars: []string{"HTTP_ADDRESS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "metrics-address",
|
||||
Usage: "Specify the local IP/port to bind the metrics server to",
|
||||
Required: false,
|
||||
Value: ":9090",
|
||||
EnvVars: []string{"METRICS_HTTP_ADDRESS"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Enable debug mode",
|
||||
|
||||
@@ -13,36 +13,29 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/bluesky-social/indigo/atproto/identity"
|
||||
"github.com/bluesky-social/indigo/util/cliutil"
|
||||
"github.com/bluesky-social/indigo/xrpc"
|
||||
"github.com/bluesky-social/social-app/bskyweb"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"github.com/labstack/echo-contrib/echoprometheus"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
echo *echo.Echo
|
||||
httpd *http.Server
|
||||
metricsHttpd *http.Server
|
||||
xrpcc *xrpc.Client
|
||||
dir identity.Directory
|
||||
echo *echo.Echo
|
||||
httpd *http.Server
|
||||
xrpcc *xrpc.Client
|
||||
dir identity.Directory
|
||||
}
|
||||
|
||||
func serve(cctx *cli.Context) error {
|
||||
debug := cctx.Bool("debug")
|
||||
httpAddress := cctx.String("http-address")
|
||||
appviewHost := cctx.String("appview-host")
|
||||
metricsAddress := cctx.String("metrics-address")
|
||||
|
||||
// Echo
|
||||
e := echo.New()
|
||||
@@ -72,28 +65,13 @@ func serve(cctx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
metricsMux := http.DefaultServeMux
|
||||
metricsMux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
metricsHttpd := &http.Server{
|
||||
Addr: metricsAddress,
|
||||
Handler: metricsMux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := metricsHttpd.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error("failed to start metrics server", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
//
|
||||
// server
|
||||
//
|
||||
server := &Server{
|
||||
echo: e,
|
||||
xrpcc: xrpcc,
|
||||
dir: identity.DefaultDirectory(),
|
||||
metricsHttpd: metricsHttpd,
|
||||
echo: e,
|
||||
xrpcc: xrpcc,
|
||||
dir: identity.DefaultDirectory(),
|
||||
}
|
||||
|
||||
// Create the HTTP server.
|
||||
@@ -154,14 +132,6 @@ func serve(cctx *cli.Context) error {
|
||||
RedirectCode: http.StatusFound,
|
||||
}))
|
||||
|
||||
echoprom := echoprometheus.NewMiddlewareWithConfig(
|
||||
echoprometheus.MiddlewareConfig{
|
||||
DoNotUseRequestPathFor404: true,
|
||||
},
|
||||
)
|
||||
|
||||
e.Use(echoprom)
|
||||
|
||||
//
|
||||
// configure routes
|
||||
//
|
||||
@@ -250,11 +220,6 @@ func (srv *Server) Shutdown() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Shutdown metrics server too
|
||||
if srv.metricsHttpd != nil {
|
||||
srv.metricsHttpd.Shutdown(ctx)
|
||||
}
|
||||
|
||||
return srv.httpd.Shutdown(ctx)
|
||||
}
|
||||
|
||||
|
||||
+22
-21
@@ -1,26 +1,24 @@
|
||||
module github.com/bluesky-social/social-app/bskyweb
|
||||
|
||||
go 1.24.5
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.4
|
||||
|
||||
require (
|
||||
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a
|
||||
github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc
|
||||
github.com/flosch/pongo2/v6 v6.0.0
|
||||
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-contrib v0.17.4
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/klauspost/compress v1.17.3
|
||||
github.com/labstack/echo/v4 v4.11.3
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
)
|
||||
|
||||
require github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
|
||||
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
@@ -28,6 +26,7 @@ require (
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
@@ -53,16 +52,17 @@ 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/gommon v0.4.2 // indirect
|
||||
github.com/labstack/gommon v0.4.1 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.12 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
@@ -73,9 +73,10 @@ 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_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.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
|
||||
@@ -93,14 +94,14 @@ 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.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/crypto v0.21.0 // indirect
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gorm.io/driver/postgres v1.5.7 // indirect
|
||||
gorm.io/driver/sqlite v1.5.5 // indirect
|
||||
gorm.io/gorm v1.25.9 // indirect
|
||||
|
||||
+44
-44
@@ -2,14 +2,14 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a h1:S12KN45uIkRglMHC8PqD/Vsz0+u3KbIaBF/6rit8/Pg=
|
||||
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q=
|
||||
github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc h1:chbGD59Cn1mw07kbq9Uvb8WUFIr1dcoL5TOhT+I9bV4=
|
||||
github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
|
||||
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
@@ -33,8 +33,10 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -100,8 +102,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
|
||||
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -111,14 +113,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
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.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/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM=
|
||||
github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
|
||||
github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk=
|
||||
github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
@@ -132,13 +130,16 @@ github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
@@ -153,8 +154,6 @@ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7B
|
||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
|
||||
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/orandin/slog-gorm v1.3.2 h1:C0lKDQPAx/pF+8K2HL7bdShPwOEJpPM0Bn80zTzxU1g=
|
||||
@@ -164,14 +163,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
|
||||
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
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.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/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
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=
|
||||
@@ -198,8 +197,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
@@ -253,8 +252,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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
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 +271,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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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=
|
||||
@@ -293,12 +292,13 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.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/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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=
|
||||
@@ -332,8 +332,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
Preconnect to essential domains
|
||||
-->
|
||||
<link rel="preconnect" href="https://bsky.social">
|
||||
<link rel="preconnect" href="https://go.bsky.app">
|
||||
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
|
||||
|
||||
<!-- Hello Humans! API docs at https://atproto.com -->
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
<meta property="og:url" content="https://bsky.app" />
|
||||
<meta name="twitter:url" content="https://bsky.app" />
|
||||
<link rel="canonical" href="https://bsky.app" />
|
||||
|
||||
<meta property="og:image" content="https://bsky.app/static/social-card-default-gradient.png" />
|
||||
<meta property="twitter:image" content="https://bsky.app/static/social-card-default-gradient.png" />
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
<meta property="profile:username" content="{{ profileView.Handle }}">
|
||||
{%- if requestURI %}
|
||||
<meta property="og:url" content="{{ requestURI }}">
|
||||
<link rel="canonical" href="{{ requestURI }}" />
|
||||
{% endif -%}
|
||||
{%- if postView.Author.DisplayName %}
|
||||
<meta property="og:title" content="{{ postView.Author.DisplayName }} (@{{ postView.Author.Handle }})">
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<meta property="profile:username" content="{{ profileView.Handle }}">
|
||||
{%- if requestURI %}
|
||||
<meta property="og:url" content="{{ requestURI }}">
|
||||
<link rel="canonical" href="{{ requestURI }}" />
|
||||
{% endif -%}
|
||||
{%- if profileView.DisplayName %}
|
||||
<meta property="og:title" content="{{ profileView.DisplayName }} (@{{ profileView.Handle }})">
|
||||
|
||||
+2
-2
@@ -89,9 +89,9 @@ If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running
|
||||
|
||||
### Adding bitdrift
|
||||
|
||||
Adding bitdrift is NOT required. You can keep `EXPO_PUBLIC_BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
|
||||
Adding bitdirft is NOT required. You can keep `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 `EXPO_PUBLIC_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 `BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
|
||||
|
||||
### Adding and Updating Locales
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
|
||||
let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch payload.action {
|
||||
case .present:
|
||||
self.presentAppStoreOverlay()
|
||||
@@ -65,18 +65,18 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
|
||||
guard let url = navigationAction.request.url else {
|
||||
return .allow
|
||||
}
|
||||
|
||||
|
||||
// Store the previous one to compare later, but only set starterPackUrl when we find the right one
|
||||
prevUrl = url
|
||||
// pathComponents starts with "/" as the first component, then each path name. so...
|
||||
// ["/", "start", "name", "rkey"]
|
||||
if isStarterPackUrl(url) {
|
||||
if isStarterPackUrl(url){
|
||||
self.starterPackUrl = url
|
||||
}
|
||||
|
||||
|
||||
return .allow
|
||||
}
|
||||
|
||||
|
||||
func isStarterPackUrl(_ url: URL) -> Bool {
|
||||
var host: String?
|
||||
if #available(iOS 16.0, *) {
|
||||
@@ -84,11 +84,11 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
|
||||
} else {
|
||||
host = url.host
|
||||
}
|
||||
|
||||
|
||||
switch host {
|
||||
case "bsky.app":
|
||||
if url.pathComponents.count == 4,
|
||||
url.pathComponents[1] == "start" || url.pathComponents[1] == "starter-pack" {
|
||||
(url.pathComponents[1] == "start" || url.pathComponents[1] == "starter-pack") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
+8
-8
@@ -19,6 +19,7 @@ import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
|
||||
class BottomSheetView(
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
@@ -30,14 +31,12 @@ class BottomSheetView(
|
||||
private lateinit var dialogRootViewGroup: DialogRootViewGroup
|
||||
private var eventDispatcher: EventDispatcher? = null
|
||||
|
||||
private val rawScreenHeight =
|
||||
context.resources.displayMetrics.heightPixels
|
||||
.toFloat()
|
||||
private val rawScreenHeight = context.resources.displayMetrics.heightPixels.toFloat()
|
||||
private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).toFloat()
|
||||
|
||||
private fun getNavigationBarHeight(): Int {
|
||||
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
|
||||
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
|
||||
}
|
||||
|
||||
private val onAttemptDismiss by EventDispatcher()
|
||||
@@ -46,7 +45,7 @@ class BottomSheetView(
|
||||
|
||||
// Props
|
||||
var disableDrag = false
|
||||
set(value) {
|
||||
set (value) {
|
||||
field = value
|
||||
this.setDraggable(!value)
|
||||
}
|
||||
@@ -158,8 +157,8 @@ class BottomSheetView(
|
||||
|
||||
// Presentation
|
||||
|
||||
private fun getHalfExpandedRatio(contentHeight: Float): Float =
|
||||
when {
|
||||
private fun getHalfExpandedRatio(contentHeight: Float): Float {
|
||||
return when {
|
||||
// Full height sheets
|
||||
contentHeight >= safeScreenHeight -> 0.99f
|
||||
// Medium height sheets (>50% but <100%)
|
||||
@@ -169,6 +168,7 @@ class BottomSheetView(
|
||||
else ->
|
||||
this.clampRatio(this.getTargetHeight() / rawScreenHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private fun present() {
|
||||
if (this.isOpen || this.isOpening || this.isClosing) return
|
||||
|
||||
+1
-4
@@ -139,10 +139,7 @@ class DialogRootViewGroup(
|
||||
return super.onHoverEvent(event)
|
||||
}
|
||||
|
||||
override fun onChildStartedNativeGesture(
|
||||
childView: View?,
|
||||
ev: MotionEvent,
|
||||
) {
|
||||
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
||||
eventDispatcher?.let { jSTouchDispatcher.onChildStartedNativeGesture(ev, it) }
|
||||
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {createPortalGroup_INTERNAL} from './lib/Portal'
|
||||
type PortalContext = React.ElementType<{children: React.ReactNode}>
|
||||
|
||||
export const Context = React.createContext({} as PortalContext)
|
||||
Context.displayName = 'BottomSheetPortalContext'
|
||||
|
||||
export const useBottomSheetPortal_INTERNAL = () => React.useContext(Context)
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ export function createPortalGroup_INTERNAL() {
|
||||
append: () => {},
|
||||
remove: () => {},
|
||||
})
|
||||
Context.displayName = 'BottomSheetPortalContext'
|
||||
|
||||
function Provider(props: React.PropsWithChildren<{}>) {
|
||||
const map = React.useRef<ComponentMap>({})
|
||||
|
||||
+60
-60
@@ -16,8 +16,8 @@ import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
class GifView(
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
) : ExpoView(context, appContext) {
|
||||
// Events
|
||||
private val onPlayerStateChange by EventDispatcher()
|
||||
@@ -82,65 +82,65 @@ class GifView(
|
||||
}
|
||||
|
||||
this.webpRequest =
|
||||
glide
|
||||
.load(source)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.skipMemoryCache(false)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
placeholderRequest?.let { glide.clear(it) }
|
||||
return false
|
||||
}
|
||||
glide.load(source)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.skipMemoryCache(false)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
placeholderRequest?.let { glide.clear(it) }
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean = true
|
||||
},
|
||||
).into(this.imageView)
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean
|
||||
): Boolean = true
|
||||
}
|
||||
)
|
||||
.into(this.imageView)
|
||||
|
||||
if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
|
||||
this.placeholderRequest =
|
||||
glide
|
||||
.load(placeholderSource)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
// Let's not bloat the memory cache with placeholders
|
||||
.skipMemoryCache(true)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
// Incase this request finishes after the webp, let's just not set
|
||||
// the drawable. This shouldn't happen because the request should
|
||||
// get cancelled
|
||||
if (imageView.drawable == null) {
|
||||
imageView.setImageDrawable(resource)
|
||||
}
|
||||
return true
|
||||
}
|
||||
glide.load(placeholderSource)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
// Let's not bloat the memory cache with placeholders
|
||||
.skipMemoryCache(true)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
// Incase this request finishes after the webp, let's just not set
|
||||
// the drawable. This shouldn't happen because the request should
|
||||
// get cancelled
|
||||
if (imageView.drawable == null) {
|
||||
imageView.setImageDrawable(resource)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean = true
|
||||
},
|
||||
).submit()
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean
|
||||
): Boolean = true
|
||||
},
|
||||
)
|
||||
.submit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,10 +174,10 @@ class GifView(
|
||||
|
||||
fun firePlayerStateChange() {
|
||||
onPlayerStateChange(
|
||||
mapOf(
|
||||
"isPlaying" to this.isPlaying,
|
||||
"isLoaded" to this.isLoaded,
|
||||
),
|
||||
mapOf(
|
||||
"isPlaying" to this.isPlaying,
|
||||
"isLoaded" to this.isLoaded,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
-6
@@ -5,12 +5,11 @@ import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import java.net.URL
|
||||
|
||||
class EmojiPickerModule : Module() {
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("EmojiPicker")
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("EmojiPicker")
|
||||
|
||||
View(EmojiPickerModuleView::class) {
|
||||
Events("onEmojiSelected")
|
||||
}
|
||||
View(EmojiPickerModuleView::class) {
|
||||
Events("onEmojiSelected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-27
@@ -8,35 +8,33 @@ import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class EmojiPickerModuleView(
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
) : ExpoView(context, appContext) {
|
||||
private var emojiView: EmojiPickerView = EmojiPickerView(context)
|
||||
private val onEmojiSelected by EventDispatcher()
|
||||
class EmojiPickerModuleView(context: Context, appContext: AppContext) :
|
||||
ExpoView(context, appContext) {
|
||||
private var emojiView: EmojiPickerView = EmojiPickerView(context)
|
||||
private val onEmojiSelected by EventDispatcher()
|
||||
|
||||
init {
|
||||
setupView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
addView(
|
||||
emojiView,
|
||||
LayoutParams(
|
||||
LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT,
|
||||
),
|
||||
)
|
||||
|
||||
emojiView.setOnEmojiPickedListener { emoji ->
|
||||
onEmojiSelected(mapOf("emoji" to emoji.emoji))
|
||||
init {
|
||||
setupView()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
removeView(emojiView)
|
||||
setupView()
|
||||
}
|
||||
private fun setupView() {
|
||||
addView(
|
||||
emojiView, LayoutParams(
|
||||
LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT
|
||||
)
|
||||
)
|
||||
|
||||
emojiView.setOnEmojiPickedListener { emoji ->
|
||||
onEmojiSelected(mapOf("emoji" to emoji.emoji))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
removeView(emojiView)
|
||||
setupView()
|
||||
}
|
||||
}
|
||||
|
||||
+7
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.107.0",
|
||||
"version": "1.105.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -12,15 +12,14 @@
|
||||
"buildFromSource": [
|
||||
"expo-notifications",
|
||||
"expo-haptics",
|
||||
"expo-media-library",
|
||||
"expo-image-picker"
|
||||
"expo-media-library"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "is-ci || husky install",
|
||||
"postinstall": "patch-package && yarn intl:compile-if-needed",
|
||||
"postinstall": "patch-package && yarn intl:compile",
|
||||
"prebuild": "expo prebuild --clean",
|
||||
"android": "expo run:android",
|
||||
"android:prod": "expo run:android --variant release",
|
||||
@@ -58,7 +57,6 @@
|
||||
"intl:extract": "lingui extract --clean --locale en",
|
||||
"intl:extract:all": "lingui extract --clean",
|
||||
"intl:compile": "lingui compile",
|
||||
"intl:compile-if-needed": "is-ci || [ -f src/locale/locales/en/messages.js ] || yarn intl:compile",
|
||||
"intl:pull": "crowdin download translations --verbose -b main",
|
||||
"intl:push": "crowdin push translations --verbose -b main",
|
||||
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
|
||||
@@ -71,7 +69,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.16.2",
|
||||
"@atproto/api": "^0.15.26",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -143,11 +141,10 @@
|
||||
"expo-file-system": "~18.1.10",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-haptics": "~14.1.4",
|
||||
"expo-image": "^2.4.0",
|
||||
"expo-image": "~2.2.1",
|
||||
"expo-image-crop-tool": "^0.1.8",
|
||||
"expo-image-manipulator": "~13.1.7",
|
||||
"expo-image-picker": "^17.0.2",
|
||||
"expo-intent-launcher": "^12.1.5",
|
||||
"expo-image-picker": "~16.1.4",
|
||||
"expo-linear-gradient": "~14.1.5",
|
||||
"expo-linking": "~7.1.5",
|
||||
"expo-localization": "~16.1.5",
|
||||
@@ -188,7 +185,6 @@
|
||||
"react-native": "^0.79.3",
|
||||
"react-native-compressor": "^1.11.0",
|
||||
"react-native-date-picker": "^5.0.12",
|
||||
"react-native-device-attest": "^0.1.6",
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-gesture-handler": "2.25.0",
|
||||
@@ -214,8 +210,6 @@
|
||||
"react-remove-scroll-bar": "^2.3.8",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"sonner": "^2.0.7",
|
||||
"sonner-native": "^0.21.0",
|
||||
"statsig-react-native-expo": "^4.6.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
@@ -224,7 +218,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.160",
|
||||
"@atproto/dev-env": "^0.3.155",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
const {withAndroidManifest} = require('@expo/config-plugins')
|
||||
|
||||
const withProcessTextQuery = config =>
|
||||
// eslint-disable-next-line no-shadow
|
||||
withAndroidManifest(config, config => {
|
||||
const manifest = config.modResults.manifest
|
||||
|
||||
// Ensure <queries> stub exists
|
||||
if (!manifest.queries) manifest.queries = [{}]
|
||||
const queries = manifest.queries[0]
|
||||
|
||||
queries.intent = queries.intent || []
|
||||
|
||||
const exists = queries.intent.some(
|
||||
i =>
|
||||
i.action?.[0]?.$?.['android:name'] ===
|
||||
'android.intent.action.PROCESS_TEXT',
|
||||
)
|
||||
|
||||
if (!exists) {
|
||||
queries.intent.push({
|
||||
action: [{$: {'android:name': 'android.intent.action.PROCESS_TEXT'}}],
|
||||
data: [{$: {'android:mimeType': 'text/plain'}}],
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
module.exports = withProcessTextQuery
|
||||
+43
-51
@@ -29,7 +29,6 @@ import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {
|
||||
beginResolveGeolocation,
|
||||
@@ -71,10 +70,8 @@ import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {ToastOutlet} from '#/components/Toast'
|
||||
import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
@@ -140,54 +137,49 @@ function InnerApp() {
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<GlobalGestureEventsProvider>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</GlobalGestureEventsProvider>
|
||||
</GestureHandlerRootView>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<GlobalGestureEventsProvider>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</GlobalGestureEventsProvider>
|
||||
</GestureHandlerRootView>
|
||||
</HideBottomBarBorderProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</VideoVolumeProvider>
|
||||
|
||||
+41
-47
@@ -18,7 +18,6 @@ import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as AgeAssuranceProvider} from '#/state/ageAssurance'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {
|
||||
beginResolveGeolocation,
|
||||
@@ -51,6 +50,7 @@ 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'
|
||||
@@ -58,11 +58,9 @@ import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
||||
import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
|
||||
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 {ToastOutlet} from '#/components/Toast'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
||||
|
||||
@@ -119,51 +117,47 @@ function InnerApp() {
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
<ToastContainer />
|
||||
</React.Fragment>
|
||||
</ActiveVideoProvider>
|
||||
</VideoVolumeProvider>
|
||||
|
||||
@@ -67,9 +67,6 @@ export const atoms = {
|
||||
zIndex: 50,
|
||||
},
|
||||
|
||||
overflow_visible: {
|
||||
overflow: 'visible',
|
||||
},
|
||||
overflow_hidden: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
@@ -332,9 +329,6 @@ export const atoms = {
|
||||
font_normal: {
|
||||
fontWeight: tokens.fontWeight.normal,
|
||||
},
|
||||
font_medium: {
|
||||
fontWeight: tokens.fontWeight.medium,
|
||||
},
|
||||
font_bold: {
|
||||
fontWeight: tokens.fontWeight.bold,
|
||||
},
|
||||
@@ -982,12 +976,6 @@ export const atoms = {
|
||||
hidden: {
|
||||
display: 'none',
|
||||
},
|
||||
inline: web({
|
||||
display: 'inline',
|
||||
}),
|
||||
block: web({
|
||||
display: 'block',
|
||||
}),
|
||||
|
||||
/*
|
||||
* Transition
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
import {type TextStyle} from 'react-native'
|
||||
import {TextStyle} from 'react-native'
|
||||
|
||||
import {isAndroid, isWeb} from '#/platform/detection'
|
||||
import {type Device, device} from '#/storage'
|
||||
import {Device, device} from '#/storage'
|
||||
|
||||
const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
|
||||
|
||||
@@ -43,11 +43,11 @@ export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
|
||||
style.fontFamily =
|
||||
{
|
||||
400: 'Inter-Regular',
|
||||
500: 'Inter-Medium',
|
||||
500: 'Inter-Regular',
|
||||
600: 'Inter-SemiBold',
|
||||
700: 'Inter-Bold',
|
||||
800: 'Inter-Bold',
|
||||
900: 'Inter-Bold',
|
||||
700: 'Inter-SemiBold',
|
||||
800: 'Inter-ExtraBold',
|
||||
900: 'Inter-ExtraBold',
|
||||
}[String(style.fontWeight || '400')] || 'Inter-Regular'
|
||||
|
||||
if (style.fontStyle === 'italic') {
|
||||
|
||||
+2
-3
@@ -8,9 +8,9 @@ import {
|
||||
setFontScale as persistFontScale,
|
||||
} from '#/alf/fonts'
|
||||
import {createThemes, defaultTheme} from '#/alf/themes'
|
||||
import {type Theme, type ThemeName} from '#/alf/types'
|
||||
import {Theme, ThemeName} from '#/alf/types'
|
||||
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||
import {type Device} from '#/storage'
|
||||
import {Device} from '#/storage'
|
||||
|
||||
export {atoms} from '#/alf/atoms'
|
||||
export * from '#/alf/breakpoints'
|
||||
@@ -61,7 +61,6 @@ export const Context = React.createContext<Alf>({
|
||||
},
|
||||
flags: {},
|
||||
})
|
||||
Context.displayName = 'AlfContext'
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
|
||||
@@ -53,7 +53,6 @@ export const borderRadius = {
|
||||
*/
|
||||
export const fontWeight = {
|
||||
normal: '400',
|
||||
medium: '500',
|
||||
bold: '600',
|
||||
heavy: '800',
|
||||
} as const
|
||||
|
||||
@@ -11,11 +11,9 @@ export function DO_NOT_USE() {
|
||||
return useFonts({
|
||||
'Inter-Regular': require('../../../assets/fonts/inter/Inter-Regular.otf'),
|
||||
'Inter-Italic': require('../../../assets/fonts/inter/Inter-Italic.otf'),
|
||||
'Inter-Medium': require('../../../assets/fonts/inter/Inter-Medium.otf'),
|
||||
'Inter-MediumItalic': require('../../../assets/fonts/inter/Inter-MediumItalic.otf'),
|
||||
'Inter-SemiBold': require('../../../assets/fonts/inter/Inter-SemiBold.otf'),
|
||||
'Inter-SemiBoldItalic': require('../../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
|
||||
'Inter-Bold': require('../../../assets/fonts/inter/Inter-Bold.otf'),
|
||||
'Inter-BoldItalic': require('../../../assets/fonts/inter/Inter-BoldItalic.otf'),
|
||||
'Inter-Bold': require('../../../assets/fonts/inter/Inter-SemiBold.otf'),
|
||||
'Inter-BoldItalic': require('../../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
|
||||
'Inter-Black': require('../../../assets/fonts/inter/Inter-ExtraBold.otf'),
|
||||
'Inter-BlackItalic': require('../../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ type Context = {
|
||||
const Context = createContext<Context>({
|
||||
type: 'info',
|
||||
})
|
||||
Context.displayName = 'AdmonitionContext'
|
||||
|
||||
export function Icon() {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import {useEffect} from 'react'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Full as Logo, Mark} from '#/components/icons/Logo'
|
||||
import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function BlockedGeoOverlay() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
useEffect(() => {
|
||||
// just counting overall hits here
|
||||
logger.metric(`blockedGeoOverlay:shown`, {})
|
||||
}, [])
|
||||
|
||||
const textStyles = [a.text_md, a.leading_normal]
|
||||
const links = {
|
||||
blog: {
|
||||
to: `https://bsky.social/about/blog/08-22-2025-mississippi-hb1126`,
|
||||
label: _(msg`Read our blog post`),
|
||||
overridePresentation: false,
|
||||
disableMismatchWarning: true,
|
||||
style: textStyles,
|
||||
},
|
||||
}
|
||||
|
||||
const blocks = [
|
||||
_(msg`Unfortunately, Bluesky is unavailable in Mississippi right now.`),
|
||||
_(
|
||||
msg`A new Mississippi law requires us to implement age verification for all users before they can access Bluesky. We think this law creates challenges that go beyond its child safety goals, and creates significant barriers that limit free speech and disproportionately harm smaller platforms and emerging technologies.`,
|
||||
),
|
||||
_(
|
||||
msg`As a small team, we cannot justify building the expensive infrastructure this requirement demands while legal challenges to this law are pending.`,
|
||||
),
|
||||
_(
|
||||
msg`For now, we have made the difficult decision to block access to Bluesky in the state of Mississippi.`,
|
||||
),
|
||||
<>
|
||||
To learn more, read our{' '}
|
||||
<InlineLinkText {...links.blog}>blog post</InlineLinkText>.
|
||||
</>,
|
||||
]
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
a.px_2xl,
|
||||
{
|
||||
paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.mx_auto,
|
||||
web({
|
||||
maxWidth: 440,
|
||||
paddingTop: gtPhone ? '8vh' : undefined,
|
||||
}),
|
||||
]}>
|
||||
<View style={[a.align_start]}>
|
||||
<View
|
||||
style={[
|
||||
a.pl_md,
|
||||
a.pr_lg,
|
||||
a.py_sm,
|
||||
a.rounded_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
{
|
||||
backgroundColor: t.palette.primary_25,
|
||||
},
|
||||
]}>
|
||||
<Mark fill={t.palette.primary_600} width={14} />
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
{
|
||||
color: t.palette.primary_600,
|
||||
},
|
||||
]}>
|
||||
<Trans>Announcement</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[a.gap_lg, {paddingTop: 32, paddingBottom: 48}]}>
|
||||
{blocks.map((block, index) => (
|
||||
<Text key={index} style={[textStyles]}>
|
||||
{block}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Logo width={120} textFill={t.atoms.text.color} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
+347
-365
@@ -14,37 +14,31 @@ import {
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {atoms as a, flatten, select, useTheme} from '#/alf'
|
||||
import {atoms as a, flatten, select, tokens, useTheme} from '#/alf'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
/**
|
||||
* 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 ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
|
||||
export type ButtonColor =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'secondary_inverted'
|
||||
| 'negative'
|
||||
| 'primary_subtle'
|
||||
| 'negative_subtle'
|
||||
| '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
|
||||
/**
|
||||
@@ -109,7 +103,6 @@ const Context = React.createContext<VariantProps & ButtonState>({
|
||||
pressed: false,
|
||||
disabled: false,
|
||||
})
|
||||
Context.displayName = 'ButtonContext'
|
||||
|
||||
export function useButtonContext() {
|
||||
return React.useContext(Context)
|
||||
@@ -138,15 +131,6 @@ 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,
|
||||
@@ -219,13 +203,8 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
const baseStyles: ViewStyle[] = []
|
||||
const hoverStyles: ViewStyle[] = []
|
||||
|
||||
/*
|
||||
* 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 (color === 'primary') {
|
||||
if (variant === 'solid') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.primary_500,
|
||||
@@ -235,30 +214,108 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.primary_200,
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_700,
|
||||
dim: t.palette.primary_300,
|
||||
dark: t.palette.primary_300,
|
||||
}),
|
||||
})
|
||||
}
|
||||
} else if (color === 'secondary') {
|
||||
} 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') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg_contrast_25)
|
||||
hoverStyles.push(t.atoms.bg_contrast_100)
|
||||
hoverStyles.push(t.atoms.bg_contrast_50)
|
||||
} else {
|
||||
baseStyles.push(t.atoms.bg_contrast_50)
|
||||
baseStyles.push(t.atoms.bg_contrast_100)
|
||||
}
|
||||
} else if (color === 'secondary_inverted') {
|
||||
} 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') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.contrast_900,
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.contrast_975,
|
||||
backgroundColor: t.palette.contrast_950,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.contrast_600,
|
||||
})
|
||||
}
|
||||
} else if (color === 'negative') {
|
||||
} 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') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.negative_500,
|
||||
@@ -268,35 +325,40 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.negative_700,
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.negative_700,
|
||||
dim: t.palette.negative_300,
|
||||
dark: t.palette.negative_300,
|
||||
}),
|
||||
})
|
||||
}
|
||||
} else if (color === 'primary_subtle') {
|
||||
} else if (variant === 'outline') {
|
||||
baseStyles.push(a.border, t.atoms.bg, {
|
||||
borderWidth: 1,
|
||||
})
|
||||
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_50,
|
||||
dim: t.palette.primary_100,
|
||||
dark: t.palette.primary_100,
|
||||
}),
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.negative_500,
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_100,
|
||||
dim: t.palette.primary_200,
|
||||
dark: t.palette.primary_200,
|
||||
}),
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: t.palette.negative_50,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_25,
|
||||
dim: t.palette.primary_50,
|
||||
dark: t.palette.primary_50,
|
||||
}),
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.negative_200,
|
||||
})
|
||||
}
|
||||
} else if (color === 'negative_subtle') {
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.negative_100,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (color === 'negative_secondary') {
|
||||
if (variant === 'solid') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
@@ -315,194 +377,80 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.negative_25,
|
||||
light: t.palette.negative_100,
|
||||
dim: t.palette.negative_50,
|
||||
dark: t.palette.negative_50,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* BEGIN DEPRECATED STYLES
|
||||
*/
|
||||
if (color === 'primary') {
|
||||
if (variant === 'outline') {
|
||||
baseStyles.push(a.border, t.atoms.bg, {
|
||||
borderWidth: 1,
|
||||
})
|
||||
} 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,
|
||||
})
|
||||
}
|
||||
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 (color === 'secondary') {
|
||||
if (variant === 'outline') {
|
||||
baseStyles.push(a.border, t.atoms.bg, {
|
||||
borderWidth: 1,
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.negative_100,
|
||||
})
|
||||
|
||||
if (!disabled) {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.contrast_300,
|
||||
})
|
||||
hoverStyles.push(t.atoms.bg_contrast_50)
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.contrast_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (color === 'secondary_inverted') {
|
||||
if (variant === 'outline') {
|
||||
baseStyles.push(a.border, t.atoms.bg, {
|
||||
borderWidth: 1,
|
||||
})
|
||||
|
||||
if (!disabled) {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.contrast_300,
|
||||
})
|
||||
hoverStyles.push(t.atoms.bg_contrast_50)
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.contrast_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (color === 'negative') {
|
||||
if (variant === 'outline') {
|
||||
baseStyles.push(a.border, t.atoms.bg, {
|
||||
borderWidth: 1,
|
||||
})
|
||||
|
||||
if (!disabled) {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.negative_500,
|
||||
})
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: t.palette.negative_50,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.negative_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.negative_100,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (color === 'negative_subtle') {
|
||||
if (variant === 'outline') {
|
||||
baseStyles.push(a.border, t.atoms.bg, {
|
||||
borderWidth: 1,
|
||||
})
|
||||
|
||||
if (!disabled) {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.negative_500,
|
||||
})
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: t.palette.negative_50,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.negative_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.negative_100,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* END DEPRECATED STYLES
|
||||
*/
|
||||
}
|
||||
|
||||
if (shape === 'default') {
|
||||
if (size === 'large') {
|
||||
baseStyles.push({
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 25,
|
||||
borderRadius: 10,
|
||||
gap: 3,
|
||||
paddingVertical: 13,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
gap: 8,
|
||||
})
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push({
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 13,
|
||||
borderRadius: 8,
|
||||
gap: 3,
|
||||
paddingVertical: 9,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 6,
|
||||
gap: 6,
|
||||
})
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push({
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 9,
|
||||
borderRadius: 6,
|
||||
gap: 2,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8,
|
||||
borderRadius: 4,
|
||||
gap: 4,
|
||||
})
|
||||
}
|
||||
} else if (shape === 'round' || shape === 'square') {
|
||||
/*
|
||||
* These sizes match the actual rendered size on screen, based on
|
||||
* Chrome's web inspector
|
||||
*/
|
||||
if (size === 'large') {
|
||||
if (shape === 'round') {
|
||||
baseStyles.push({height: 44, width: 44})
|
||||
baseStyles.push({height: 46, width: 46})
|
||||
} else {
|
||||
baseStyles.push({height: 44, width: 44})
|
||||
}
|
||||
} else if (size === 'small') {
|
||||
if (shape === 'round') {
|
||||
baseStyles.push({height: 33, width: 33})
|
||||
baseStyles.push({height: 34, width: 34})
|
||||
} else {
|
||||
baseStyles.push({height: 33, width: 33})
|
||||
baseStyles.push({height: 34, width: 34})
|
||||
}
|
||||
} else if (size === 'tiny') {
|
||||
if (shape === 'round') {
|
||||
baseStyles.push({height: 25, width: 25})
|
||||
baseStyles.push({height: 22, width: 22})
|
||||
} else {
|
||||
baseStyles.push({height: 25, width: 25})
|
||||
baseStyles.push({height: 21, width: 21})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,9 +458,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
baseStyles.push(a.rounded_full)
|
||||
} else if (shape === 'square') {
|
||||
if (size === 'tiny') {
|
||||
baseStyles.push({
|
||||
borderRadius: 6,
|
||||
})
|
||||
baseStyles.push(a.rounded_xs)
|
||||
} else {
|
||||
baseStyles.push(a.rounded_sm)
|
||||
}
|
||||
@@ -525,6 +471,49 @@ 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,7 +545,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.curve_continuous,
|
||||
flattenedBaseStyles,
|
||||
...(state.hovered || state.pressed
|
||||
? [hoverStyles, flatten(hoverStyleProp)]
|
||||
@@ -568,6 +556,27 @@ 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>
|
||||
@@ -583,187 +592,159 @@ export function useSharedButtonTextStyles() {
|
||||
return React.useMemo(() => {
|
||||
const baseStyles: TextStyle[] = []
|
||||
|
||||
/*
|
||||
* 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 (color === 'primary') {
|
||||
if (variant === 'solid') {
|
||||
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: select(t.name, {
|
||||
light: t.palette.white,
|
||||
dim: t.atoms.text_inverted.color,
|
||||
dark: t.atoms.text_inverted.color,
|
||||
}),
|
||||
color: t.palette.primary_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({color: t.palette.primary_600})
|
||||
} else {
|
||||
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
|
||||
}
|
||||
}
|
||||
} else if (color === 'secondary') {
|
||||
if (variant === 'solid' || variant === 'gradient') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_700,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_400,
|
||||
})
|
||||
}
|
||||
} else if (color === 'secondary') {
|
||||
} else if (variant === 'outline') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.text_contrast_medium)
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_300,
|
||||
})
|
||||
}
|
||||
} else if (color === 'secondary_inverted') {
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.text_inverted)
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_300,
|
||||
})
|
||||
}
|
||||
} else if (color === 'negative') {
|
||||
}
|
||||
} else if (color === 'secondary_inverted') {
|
||||
if (variant === 'solid' || variant === 'gradient') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: t.palette.contrast_50,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
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') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({color: t.palette.white})
|
||||
} else {
|
||||
baseStyles.push({color: t.palette.negative_300})
|
||||
baseStyles.push({color: t.palette.white, opacity: 0.5})
|
||||
}
|
||||
} else if (color === 'primary_subtle') {
|
||||
} 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') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.primary_600,
|
||||
dim: t.palette.primary_800,
|
||||
dark: t.palette.primary_800,
|
||||
light: t.palette.negative_500,
|
||||
dim: t.palette.negative_950,
|
||||
dark: t.palette.negative_900,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.primary_200,
|
||||
dim: t.palette.primary_200,
|
||||
dark: t.palette.primary_200,
|
||||
light: t.palette.negative_500,
|
||||
dim: t.palette.negative_700,
|
||||
dark: t.palette.negative_700,
|
||||
}),
|
||||
opacity: 0.5,
|
||||
})
|
||||
}
|
||||
} else if (color === 'negative_subtle') {
|
||||
} else if (variant === 'outline') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.negative_600,
|
||||
dim: t.palette.negative_800,
|
||||
dark: t.palette.negative_800,
|
||||
}),
|
||||
})
|
||||
baseStyles.push({color: t.palette.negative_400})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.negative_200,
|
||||
dim: t.palette.negative_200,
|
||||
dark: t.palette.negative_200,
|
||||
}),
|
||||
})
|
||||
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 {
|
||||
/*
|
||||
* 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_subtle') {
|
||||
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})
|
||||
}
|
||||
}
|
||||
if (!disabled) {
|
||||
baseStyles.push({color: t.palette.white})
|
||||
} else {
|
||||
baseStyles.push({color: t.palette.white, opacity: 0.5})
|
||||
}
|
||||
/*
|
||||
* END DEPRECATED STYLES
|
||||
*/
|
||||
}
|
||||
|
||||
if (size === 'large') {
|
||||
baseStyles.push(a.text_md, a.leading_snug, a.font_medium)
|
||||
baseStyles.push(a.text_md, a.leading_tight)
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push(a.text_sm, a.leading_snug, a.font_medium)
|
||||
baseStyles.push(a.text_sm, a.leading_tight)
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push(a.text_xs, a.leading_snug, a.font_medium)
|
||||
baseStyles.push(a.text_xs, a.leading_tight)
|
||||
}
|
||||
|
||||
return StyleSheet.flatten(baseStyles)
|
||||
@@ -774,7 +755,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
||||
const textStyles = useSharedButtonTextStyles()
|
||||
|
||||
return (
|
||||
<Text {...rest} style={[a.text_center, textStyles, style]}>
|
||||
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
@@ -782,16 +763,14 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
|
||||
|
||||
export function ButtonIcon({
|
||||
icon: Comp,
|
||||
position,
|
||||
size,
|
||||
}: {
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
/**
|
||||
* @deprecated no longer needed
|
||||
*/
|
||||
position?: 'left' | 'right'
|
||||
size?: SVGIconProps['size']
|
||||
}) {
|
||||
const {size: buttonSize} = useButtonContext()
|
||||
const {size: buttonSize, disabled} = useButtonContext()
|
||||
const textStyles = useSharedButtonTextStyles()
|
||||
const {iconSize, iconContainerSize} = React.useMemo(() => {
|
||||
/**
|
||||
@@ -800,7 +779,7 @@ export function ButtonIcon({
|
||||
const iconSizeShorthand =
|
||||
size ??
|
||||
(({
|
||||
large: 'md',
|
||||
large: 'sm',
|
||||
small: 'sm',
|
||||
tiny: 'xs',
|
||||
}[buttonSize || 'small'] || 'sm') as Exclude<
|
||||
@@ -815,7 +794,7 @@ export function ButtonIcon({
|
||||
const iconSize = {
|
||||
xs: 12,
|
||||
sm: 16,
|
||||
md: 18,
|
||||
md: 20,
|
||||
lg: 24,
|
||||
xl: 28,
|
||||
'2xl': 32,
|
||||
@@ -826,9 +805,9 @@ export function ButtonIcon({
|
||||
* don't increase button size
|
||||
*/
|
||||
const iconContainerSize = {
|
||||
large: 20,
|
||||
small: 17,
|
||||
tiny: 15,
|
||||
large: 18,
|
||||
small: 16,
|
||||
tiny: 13,
|
||||
}[buttonSize || 'small']
|
||||
|
||||
return {
|
||||
@@ -844,6 +823,9 @@ export function ButtonIcon({
|
||||
{
|
||||
width: iconContainerSize,
|
||||
height: iconContainerSize,
|
||||
opacity: disabled ? 0.7 : 1,
|
||||
marginLeft: position === 'left' ? -2 : 0,
|
||||
marginRight: position === 'right' ? -2 : 0,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
|
||||
@@ -7,13 +7,10 @@ import {
|
||||
} from '#/components/ContextMenu/types'
|
||||
|
||||
export const Context = React.createContext<ContextType | null>(null)
|
||||
Context.displayName = 'ContextMenuContext'
|
||||
|
||||
export const MenuContext = React.createContext<MenuContextType | null>(null)
|
||||
MenuContext.displayName = 'ContextMenuMenuContext'
|
||||
|
||||
export const ItemContext = React.createContext<ItemContextType | null>(null)
|
||||
ItemContext.displayName = 'ContextMenuItemContext'
|
||||
|
||||
export function useContextMenuContext() {
|
||||
const context = React.useContext(Context)
|
||||
|
||||
@@ -23,7 +23,6 @@ export const Context = createContext<DialogContextProps>({
|
||||
setDisableDrag: () => {},
|
||||
isWithinDialog: false,
|
||||
})
|
||||
Context.displayName = 'DialogContext'
|
||||
|
||||
export function useDialogContext() {
|
||||
return useContext(Context)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -8,7 +9,6 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
@@ -29,13 +29,14 @@ import {Button} from '#/components/Button'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
|
||||
import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {ProgressGuideList} from './ProgressGuide/List'
|
||||
|
||||
const MOBILE_CARD_WIDTH = 165
|
||||
const MOBILE_CARD_WIDTH = 300
|
||||
|
||||
function CardOuter({
|
||||
children,
|
||||
@@ -46,13 +47,11 @@ function CardOuter({
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.w_full,
|
||||
a.p_md,
|
||||
a.rounded_lg,
|
||||
a.p_lg,
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
t.atoms.bg,
|
||||
t.atoms.shadow_sm,
|
||||
t.atoms.border_contrast_low,
|
||||
!gtMobile && {
|
||||
width: MOBILE_CARD_WIDTH,
|
||||
@@ -65,27 +64,23 @@ function CardOuter({
|
||||
}
|
||||
|
||||
export function SuggestedFollowPlaceholder() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<CardOuter>
|
||||
<ProfileCard.Outer>
|
||||
<View
|
||||
style={[a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto]}>
|
||||
<ProfileCard.AvatarPlaceholder size={88} />
|
||||
<ProfileCard.NamePlaceholder />
|
||||
<View style={[a.w_full]}>
|
||||
<ProfileCard.DescriptionPlaceholder numberOfLines={2} />
|
||||
</View>
|
||||
</View>
|
||||
<CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.AvatarPlaceholder />
|
||||
<ProfileCard.NameAndHandlePlaceholder />
|
||||
</ProfileCard.Header>
|
||||
|
||||
<ProfileCard.FollowButtonPlaceholder />
|
||||
</ProfileCard.Outer>
|
||||
<ProfileCard.DescriptionPlaceholder numberOfLines={2} />
|
||||
</CardOuter>
|
||||
)
|
||||
}
|
||||
|
||||
export function SuggestedFeedsCardPlaceholder() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<CardOuter style={[a.gap_sm]}>
|
||||
<CardOuter style={[a.gap_sm, t.atoms.border_contrast_low]}>
|
||||
<FeedCard.Header>
|
||||
<FeedCard.AvatarPlaceholder />
|
||||
<FeedCard.TitleAndBylinePlaceholder creator />
|
||||
@@ -243,208 +238,171 @@ export function ProfileGrid({
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
recId?: number
|
||||
error: Error | null
|
||||
viewContext: 'profile' | 'profileHeader' | 'feed'
|
||||
viewContext: 'profile' | 'feed'
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
const isLoading = isSuggestionsLoading || !moderationOpts
|
||||
const isProfileHeaderContext = viewContext === 'profileHeader'
|
||||
const isFeedContext = viewContext === 'feed'
|
||||
const maxLength = gtMobile ? 4 : 6
|
||||
|
||||
const maxLength = gtMobile ? 3 : isProfileHeaderContext ? 12 : 6
|
||||
const minLength = gtMobile ? 3 : 4
|
||||
|
||||
const content = isLoading
|
||||
? Array(maxLength)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[
|
||||
a.flex_1,
|
||||
gtMobile &&
|
||||
web([
|
||||
a.flex_0,
|
||||
a.flex_grow,
|
||||
{width: `calc(30% - ${a.gap_md.gap / 2}px)`},
|
||||
]),
|
||||
]}>
|
||||
<SuggestedFollowPlaceholder />
|
||||
</View>
|
||||
))
|
||||
: error || !profiles.length
|
||||
? null
|
||||
: profiles.slice(0, maxLength).map((profile, index) => (
|
||||
<ProfileCard.Link
|
||||
key={profile.did}
|
||||
profile={profile}
|
||||
onPress={() => {
|
||||
logEvent('suggestedUser:press', {
|
||||
logContext: isFeedContext
|
||||
const content = isLoading ? (
|
||||
Array(maxLength)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}])]}>
|
||||
<SuggestedFollowPlaceholder />
|
||||
</View>
|
||||
))
|
||||
) : error || !profiles.length ? null : (
|
||||
<>
|
||||
{profiles.slice(0, maxLength).map((profile, index) => (
|
||||
<ProfileCard.Link
|
||||
key={profile.did}
|
||||
profile={profile}
|
||||
onPress={() => {
|
||||
logEvent('suggestedUser:press', {
|
||||
logContext:
|
||||
viewContext === 'feed'
|
||||
? 'InterstitialDiscover'
|
||||
: 'InterstitialProfile',
|
||||
recId,
|
||||
position: index,
|
||||
})
|
||||
}}
|
||||
style={[
|
||||
a.flex_1,
|
||||
gtMobile &&
|
||||
web([
|
||||
a.flex_0,
|
||||
a.flex_grow,
|
||||
{width: `calc(30% - ${a.gap_md.gap / 2}px)`},
|
||||
]),
|
||||
]}>
|
||||
{({hovered, pressed}) => (
|
||||
<CardOuter
|
||||
style={[(hovered || pressed) && t.atoms.border_contrast_high]}>
|
||||
<ProfileCard.Outer>
|
||||
<View
|
||||
style={[
|
||||
a.flex_col,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
a.pb_sm,
|
||||
a.mb_auto,
|
||||
]}>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
disabledPreview
|
||||
size={88}
|
||||
/>
|
||||
<View style={[a.flex_col, a.align_center, a.max_w_full]}>
|
||||
<ProfileCard.Name
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.Description
|
||||
profile={profile}
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
t.atoms.text_contrast_medium,
|
||||
a.text_center,
|
||||
a.text_xs,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
recId,
|
||||
position: index,
|
||||
})
|
||||
}}
|
||||
style={[
|
||||
a.flex_1,
|
||||
gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}]),
|
||||
]}>
|
||||
{({hovered, pressed}) => (
|
||||
<CardOuter
|
||||
style={[
|
||||
a.flex_1,
|
||||
(hovered || pressed) && t.atoms.border_contrast_high,
|
||||
]}>
|
||||
<ProfileCard.Outer>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.FollowButton
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
logContext="FeedInterstitial"
|
||||
withIcon={false}
|
||||
style={[a.rounded_sm]}
|
||||
shape="round"
|
||||
colorInverted
|
||||
onFollow={() => {
|
||||
logEvent('suggestedUser:follow', {
|
||||
logContext: isFeedContext
|
||||
? 'InterstitialDiscover'
|
||||
: 'InterstitialProfile',
|
||||
logContext:
|
||||
viewContext === 'feed'
|
||||
? 'InterstitialDiscover'
|
||||
: 'InterstitialProfile',
|
||||
location: 'Card',
|
||||
recId,
|
||||
position: index,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</ProfileCard.Outer>
|
||||
</CardOuter>
|
||||
)}
|
||||
</ProfileCard.Link>
|
||||
))
|
||||
</ProfileCard.Header>
|
||||
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
||||
</ProfileCard.Outer>
|
||||
</CardOuter>
|
||||
)}
|
||||
</ProfileCard.Link>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
if (error || (!isLoading && profiles.length < minLength)) {
|
||||
if (error || (!isLoading && profiles.length < 4)) {
|
||||
logger.debug(`Not enough profiles to show suggested follows`)
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
!isProfileHeaderContext && a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}>
|
||||
<View
|
||||
style={[
|
||||
a.px_lg,
|
||||
a.pt_md,
|
||||
a.p_lg,
|
||||
a.pb_xs,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
]}
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
<Text style={[a.text_sm, a.font_bold, t.atoms.text]}>
|
||||
{isFeedContext ? (
|
||||
<Trans>Suggested for you</Trans>
|
||||
) : (
|
||||
]}>
|
||||
<Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}>
|
||||
{viewContext === 'profile' ? (
|
||||
<Trans>Similar accounts</Trans>
|
||||
) : (
|
||||
<Trans>Suggested for you</Trans>
|
||||
)}
|
||||
</Text>
|
||||
{!isProfileHeaderContext && (
|
||||
<InlineLinkText
|
||||
label={_(msg`See more suggested profiles on the Explore page`)}
|
||||
to="/search">
|
||||
<Trans>See more</Trans>
|
||||
</InlineLinkText>
|
||||
)}
|
||||
<Person fill={t.atoms.text_contrast_low.color} size="sm" />
|
||||
</View>
|
||||
|
||||
{gtMobile ? (
|
||||
<View style={[a.p_lg, a.pt_md]}>
|
||||
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
|
||||
<View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}>
|
||||
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}>
|
||||
{content}
|
||||
</View>
|
||||
|
||||
<View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Browse more suggestions`)}
|
||||
to="/search"
|
||||
style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Browse more suggestions</Trans>
|
||||
</InlineLinkText>
|
||||
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<BlockDrawerGesture>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={[a.p_lg, a.pt_md, a.flex_row, a.gap_md]}
|
||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||
decelerationRate="fast">
|
||||
{content}
|
||||
<View>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||
decelerationRate="fast">
|
||||
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
|
||||
{content}
|
||||
|
||||
{!isProfileHeaderContext && <SeeMoreSuggestedProfilesCard />}
|
||||
</ScrollView>
|
||||
<Button
|
||||
label={_(msg`Browse more accounts on the Explore page`)}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}>
|
||||
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_row, a.px_lg]}>
|
||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||
<Trans>
|
||||
Browse more suggestions on the Explore page
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Arrow size="xl" />
|
||||
</View>
|
||||
</View>
|
||||
</CardOuter>
|
||||
</Button>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</BlockDrawerGesture>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SeeMoreSuggestedProfilesCard() {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={_(msg`Browse more accounts on the Explore page`)}
|
||||
style={[a.flex_col]}
|
||||
onPress={() => {
|
||||
navigation.navigate('SearchTab')
|
||||
}}>
|
||||
<CardOuter>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_col, a.align_center, a.gap_md]}>
|
||||
<Text style={[a.leading_snug, a.text_center]}>
|
||||
<Trans>See more accounts you might like</Trans>
|
||||
</Text>
|
||||
|
||||
<Arrow size="xl" />
|
||||
</View>
|
||||
</View>
|
||||
</CardOuter>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function SuggestedFeeds() {
|
||||
const numFeedsToDisplay = 3
|
||||
const t = useTheme()
|
||||
@@ -484,7 +442,10 @@ export function SuggestedFeeds() {
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<CardOuter
|
||||
style={[(hovered || pressed) && t.atoms.border_contrast_high]}>
|
||||
style={[
|
||||
a.flex_1,
|
||||
(hovered || pressed) && t.atoms.border_contrast_high,
|
||||
]}>
|
||||
<FeedCard.Outer>
|
||||
<FeedCard.Header>
|
||||
<FeedCard.Avatar src={feed.avatar} />
|
||||
@@ -558,7 +519,7 @@ export function SuggestedFeeds() {
|
||||
navigation.navigate('SearchTab')
|
||||
}}
|
||||
style={[a.flex_col]}>
|
||||
<CardOuter>
|
||||
<CardOuter style={[a.flex_1]}>
|
||||
<View style={[a.flex_1, a.justify_center]}>
|
||||
<View style={[a.flex_row, a.px_lg]}>
|
||||
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import {
|
||||
AccessibilityInfo,
|
||||
findNodeHandle,
|
||||
Pressable,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useA11y} from '#/state/a11y'
|
||||
|
||||
/**
|
||||
* Conditionally wraps children in a `FocusTrap` component based on whether
|
||||
* screen reader support is enabled. THIS SHOULD BE USED SPARINGLY, only when
|
||||
* no better option is available.
|
||||
*/
|
||||
export function FocusScope({children}: {children: ReactNode}) {
|
||||
const {screenReaderEnabled} = useA11y()
|
||||
|
||||
return screenReaderEnabled ? <FocusTrap>{children}</FocusTrap> : children
|
||||
}
|
||||
|
||||
/**
|
||||
* `FocusTrap` is intended as a last-ditch effort to ensure that users keep
|
||||
* focus within a certain section of the app, like an overlay.
|
||||
*
|
||||
* It works by placing "guards" at the start and end of the active content.
|
||||
* Then when the user reaches either of those guards, it will announce that
|
||||
* they have reached the start or end of the content and tell them how to
|
||||
* remain within the active content section.
|
||||
*/
|
||||
function FocusTrap({children}: {children: ReactNode}) {
|
||||
const {_} = useLingui()
|
||||
const child = useRef<View>(null)
|
||||
|
||||
/*
|
||||
* Here we add a ref to the first child of this component. This currently
|
||||
* overrides any ref already on that first child, so we throw an error here
|
||||
* to prevent us from ever accidentally doing this.
|
||||
*/
|
||||
const decoratedChildren = useMemo(() => {
|
||||
return Children.toArray(children).map((node, i) => {
|
||||
if (i === 0 && isValidElement(node)) {
|
||||
const n = node as ReactElement<any>
|
||||
if (n.props.ref !== undefined) {
|
||||
throw new Error(
|
||||
'FocusScope needs to override the ref on its first child.',
|
||||
)
|
||||
}
|
||||
return cloneElement(n, {
|
||||
...n.props,
|
||||
ref: child,
|
||||
})
|
||||
}
|
||||
return node
|
||||
})
|
||||
}, [children])
|
||||
|
||||
const focusNode = useCallback((ref: View | null) => {
|
||||
if (!ref) return
|
||||
const node = findNodeHandle(ref)
|
||||
if (node) {
|
||||
AccessibilityInfo.setAccessibilityFocus(node)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
focusNode(child.current)
|
||||
}, 1e3)
|
||||
}, [focusNode])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
accessible
|
||||
accessibilityLabel={_(
|
||||
msg`You've reached the start of the active content.`,
|
||||
)}
|
||||
accessibilityHint={_(
|
||||
msg`Please go back, or activate this element to return to the start of the active content.`,
|
||||
)}
|
||||
accessibilityActions={[{name: 'activate', label: 'activate'}]}
|
||||
onAccessibilityAction={event => {
|
||||
switch (event.nativeEvent.actionName) {
|
||||
case 'activate': {
|
||||
focusNode(child.current)
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<Noop />
|
||||
</Pressable>
|
||||
<View
|
||||
/**
|
||||
* This property traps focus effectively on iOS, but not on Android.
|
||||
*/
|
||||
accessibilityViewIsModal>
|
||||
{decoratedChildren}
|
||||
</View>
|
||||
<Pressable
|
||||
accessibilityLabel={_(
|
||||
msg`You've reached the end of the active content.`,
|
||||
)}
|
||||
accessibilityHint={_(
|
||||
msg`Please go back, or activate this element to return to the start of the active content.`,
|
||||
)}
|
||||
accessibilityActions={[{name: 'activate', label: 'activate'}]}
|
||||
onAccessibilityAction={event => {
|
||||
switch (event.nativeEvent.actionName) {
|
||||
case 'activate': {
|
||||
focusNode(child.current)
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<Noop />
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Noop() {
|
||||
return (
|
||||
<Text
|
||||
accessible={false}
|
||||
style={{
|
||||
height: 1,
|
||||
opacity: 0,
|
||||
}}>
|
||||
{' '}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import {type ReactNode} from 'react'
|
||||
import {FocusScope as RadixFocusScope} from 'radix-ui/internal'
|
||||
|
||||
/*
|
||||
* The web version of the FocusScope component is a proper implementation, we
|
||||
* use this in Dialogs and such already. It's here as a convenient counterpart
|
||||
* to the hacky native solution.
|
||||
*/
|
||||
export function FocusScope({children}: {children: ReactNode}) {
|
||||
return (
|
||||
<RadixFocusScope.FocusScope loop asChild trapped>
|
||||
{children}
|
||||
</RadixFocusScope.FocusScope>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import {createContext, useContext, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, type ViewStyleProp} from '#/alf'
|
||||
import {atoms as a, ViewStyleProp} from '#/alf'
|
||||
|
||||
const Context = createContext({
|
||||
gap: 0,
|
||||
})
|
||||
Context.displayName = 'GridContext'
|
||||
|
||||
export function Row({
|
||||
children,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
moderateProfile,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -12,9 +8,9 @@ import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Link, type LinkProps} from '#/components/Link'
|
||||
import {Link, LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const AVI_SIZE = 30
|
||||
const AVI_SIZE_SMALL = 20
|
||||
@@ -141,9 +137,9 @@ function KnownFollowersInner({
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
{
|
||||
height: SIZE,
|
||||
width: SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||
},
|
||||
pressed && {
|
||||
opacity: 0.5,
|
||||
@@ -153,14 +149,15 @@ function KnownFollowersInner({
|
||||
<View
|
||||
key={prof.did}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.rounded_full,
|
||||
{
|
||||
borderWidth: AVI_BORDER,
|
||||
borderColor: t.atoms.bg.backgroundColor,
|
||||
width: SIZE + AVI_BORDER * 2,
|
||||
height: SIZE + AVI_BORDER * 2,
|
||||
left: i * a.gap_md.gap,
|
||||
zIndex: AVI_BORDER - i,
|
||||
marginLeft: i > 0 ? -8 : 0,
|
||||
},
|
||||
]}>
|
||||
<UserAvatar
|
||||
@@ -168,7 +165,6 @@ function KnownFollowersInner({
|
||||
avatar={prof.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={prof.associated?.labeler ? 'labeler' : 'user'}
|
||||
noBorder
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
|
||||
@@ -77,7 +77,6 @@ export function Outer({
|
||||
}
|
||||
|
||||
const AlignmentContext = createContext<'platform' | 'left'>('platform')
|
||||
AlignmentContext.displayName = 'AlignmentContext'
|
||||
|
||||
export function Content({
|
||||
children,
|
||||
|
||||
@@ -3,4 +3,3 @@ import React from 'react'
|
||||
export const ScrollbarOffsetContext = React.createContext({
|
||||
isWithinOffsetView: false,
|
||||
})
|
||||
ScrollbarOffsetContext.displayName = 'ScrollbarOffsetContext'
|
||||
|
||||
+1
-87
@@ -1,5 +1,5 @@
|
||||
import React, {useMemo} from 'react'
|
||||
import {type GestureResponderEvent, Linking} from 'react-native'
|
||||
import {type GestureResponderEvent} from 'react-native'
|
||||
import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||
import {
|
||||
type LinkProps as RNLinkProps,
|
||||
@@ -13,7 +13,6 @@ import {type AllNavigatorParams, type RouteParams} from '#/lib/routes/types'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {
|
||||
convertBskyAppUrlIfNeeded,
|
||||
createProxiedUrl,
|
||||
isBskyDownloadUrl,
|
||||
isExternalUrl,
|
||||
linkRequiresWarning,
|
||||
@@ -408,91 +407,6 @@ export function InlineLinkText({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A barebones version of `InlineLinkText`, for use outside a
|
||||
* `react-navigation` context.
|
||||
*/
|
||||
export function SimpleInlineLinkText({
|
||||
children,
|
||||
to,
|
||||
style,
|
||||
download,
|
||||
selectable,
|
||||
label,
|
||||
disableUnderline,
|
||||
shouldProxy,
|
||||
...rest
|
||||
}: Omit<
|
||||
InlineLinkProps,
|
||||
| 'to'
|
||||
| 'action'
|
||||
| 'disableMismatchWarning'
|
||||
| 'overridePresentation'
|
||||
| 'onPress'
|
||||
| 'onLongPress'
|
||||
| 'shareOnLongPress'
|
||||
> & {
|
||||
to: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onHoverIn,
|
||||
onOut: onHoverOut,
|
||||
} = useInteractionState()
|
||||
const flattenedStyle = flatten(style) || {}
|
||||
const isExternal = isExternalUrl(to)
|
||||
|
||||
let href = to
|
||||
if (shouldProxy) {
|
||||
href = createProxiedUrl(href)
|
||||
}
|
||||
|
||||
const onPress = () => {
|
||||
Linking.openURL(href)
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
selectable={selectable}
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={label}
|
||||
{...rest}
|
||||
style={[
|
||||
{color: t.palette.primary_500},
|
||||
hovered &&
|
||||
!disableUnderline && {
|
||||
...web({
|
||||
outline: 0,
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationColor:
|
||||
flattenedStyle.color ?? t.palette.primary_500,
|
||||
}),
|
||||
},
|
||||
flattenedStyle,
|
||||
]}
|
||||
role="link"
|
||||
onPress={onPress}
|
||||
onMouseEnter={onHoverIn}
|
||||
onMouseLeave={onHoverOut}
|
||||
accessibilityRole="link"
|
||||
href={href}
|
||||
{...web({
|
||||
hrefAttrs: {
|
||||
target: download ? undefined : isExternal ? 'blank' : undefined,
|
||||
rel: isExternal ? 'noopener noreferrer' : undefined,
|
||||
download,
|
||||
},
|
||||
dataSet: {
|
||||
// default to no underline, apply this ourselves
|
||||
noUnderline: '1',
|
||||
},
|
||||
})}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function WebOnlyInlineLinkText({
|
||||
children,
|
||||
to,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {memo} from 'react'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import React, {memo} from 'react'
|
||||
import {StyleProp, View, 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'
|
||||
@@ -96,7 +95,7 @@ function ListFooterMaybeError({
|
||||
)}
|
||||
</Text>
|
||||
<Button
|
||||
variant="solid"
|
||||
variant="gradient"
|
||||
label={_(msg`Press to retry`)}
|
||||
style={[
|
||||
a.align_center,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function LockScroll() {
|
||||
return null
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||
|
||||
export const LockScroll = RemoveScrollBar
|
||||
@@ -3,10 +3,8 @@ import React from 'react'
|
||||
import {type ContextType, type ItemContextType} from '#/components/Menu/types'
|
||||
|
||||
export const Context = React.createContext<ContextType | null>(null)
|
||||
Context.displayName = 'MenuContext'
|
||||
|
||||
export const ItemContext = React.createContext<ItemContextType | null>(null)
|
||||
ItemContext.displayName = 'MenuItemContext'
|
||||
|
||||
export function useMenuContext() {
|
||||
const context = React.useContext(Context)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import {View} from 'react-native'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function Badge() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View style={[a.align_start]}>
|
||||
<View
|
||||
style={[
|
||||
a.pl_md,
|
||||
a.pr_lg,
|
||||
a.py_sm,
|
||||
a.rounded_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
{
|
||||
backgroundColor: t.palette.primary_25,
|
||||
},
|
||||
]}>
|
||||
<Logo fill={t.palette.primary_600} width={14} />
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
{
|
||||
color: t.palette.primary_600,
|
||||
},
|
||||
]}>
|
||||
<Trans>Announcement</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import {type ReactNode} from 'react'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {
|
||||
useSafeAreaFrame,
|
||||
useSafeAreaInsets,
|
||||
} from 'react-native-safe-area-context'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {isAndroid, isNative} from '#/platform/detection'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {transparentifyColor} from '#/alf/util/colorGeneration'
|
||||
import {FocusScope} from '#/components/FocusScope'
|
||||
import {LockScroll} from '#/components/LockScroll'
|
||||
|
||||
const GUTTER = 24
|
||||
|
||||
export function Overlay({
|
||||
children,
|
||||
label,
|
||||
}: {
|
||||
children: ReactNode
|
||||
label: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const {reduceMotionEnabled} = useA11y()
|
||||
const insets = useSafeAreaInsets()
|
||||
const frame = useSafeAreaFrame()
|
||||
|
||||
return (
|
||||
<>
|
||||
<LockScroll />
|
||||
|
||||
<View style={[a.fixed, a.inset_0, !reduceMotionEnabled && a.fade_in]}>
|
||||
{gtPhone ? (
|
||||
<View style={[a.absolute, a.inset_0, {opacity: 0.8}]}>
|
||||
<View
|
||||
style={[
|
||||
a.fixed,
|
||||
a.inset_0,
|
||||
{backgroundColor: t.palette.black},
|
||||
!reduceMotionEnabled && a.fade_in,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<LinearGradient
|
||||
colors={[
|
||||
transparentifyColor(t.atoms.bg.backgroundColor, 0),
|
||||
t.atoms.bg.backgroundColor,
|
||||
t.atoms.bg.backgroundColor,
|
||||
]}
|
||||
start={[0.5, 0]}
|
||||
end={[0.5, 1]}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[
|
||||
a.z_10,
|
||||
gtPhone &&
|
||||
web({
|
||||
paddingHorizontal: GUTTER,
|
||||
paddingVertical: '10vh',
|
||||
}),
|
||||
]}
|
||||
contentContainerStyle={[a.align_center]}>
|
||||
{/**
|
||||
* This is needed to prevent centered dialogs from overflowing
|
||||
* above the screen, and provides a "natural" centering so that
|
||||
* stacked dialogs appear relatively aligned.
|
||||
*/}
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.z_20,
|
||||
a.align_center,
|
||||
!gtPhone && [a.justify_end, {minHeight: frame.height}],
|
||||
isNative && [
|
||||
{
|
||||
paddingBottom: Math.max(insets.bottom, a.p_2xl.padding),
|
||||
},
|
||||
],
|
||||
]}>
|
||||
{!gtPhone && (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.w_full,
|
||||
{
|
||||
minHeight: Math.max(insets.top, a.p_2xl.padding),
|
||||
},
|
||||
]}>
|
||||
<LinearGradient
|
||||
colors={[
|
||||
transparentifyColor(t.atoms.bg.backgroundColor, 0),
|
||||
t.atoms.bg.backgroundColor,
|
||||
]}
|
||||
start={[0.5, 0]}
|
||||
end={[0.5, 1]}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<FocusScope>
|
||||
<View
|
||||
accessible={isAndroid}
|
||||
role="dialog"
|
||||
aria-role="dialog"
|
||||
aria-label={label}
|
||||
style={flatten([
|
||||
a.relative,
|
||||
a.w_full,
|
||||
a.p_2xl,
|
||||
t.atoms.bg,
|
||||
!reduceMotionEnabled && a.zoom_fade_in,
|
||||
gtPhone && [
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
t.atoms.shadow_lg,
|
||||
t.atoms.border_contrast_low,
|
||||
web({
|
||||
maxWidth: 420,
|
||||
}),
|
||||
],
|
||||
])}>
|
||||
{children}
|
||||
</View>
|
||||
</FocusScope>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
|
||||
const portalGroup = createPortalGroup()
|
||||
|
||||
export const Provider = portalGroup.Provider
|
||||
export const Portal = portalGroup.Portal
|
||||
export const Outlet = portalGroup.Outlet
|
||||
@@ -1,195 +0,0 @@
|
||||
import {describe, test} from '@jest/globals'
|
||||
|
||||
import {
|
||||
computeCompletedState,
|
||||
syncCompletedState,
|
||||
} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
|
||||
|
||||
jest.mock('../../../state/queries/nuxs')
|
||||
|
||||
describe('computeCompletedState', () => {
|
||||
test(`initial state`, () => {
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady: false,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: undefined,
|
||||
})
|
||||
|
||||
expect(completed).toBe(true)
|
||||
})
|
||||
|
||||
test(`nux loaded state`, () => {
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: undefined,
|
||||
})
|
||||
|
||||
expect(completed).toBe(false)
|
||||
})
|
||||
|
||||
test(`nux saving state`, () => {
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: true,
|
||||
completedForDevice: undefined,
|
||||
})
|
||||
|
||||
expect(completed).toBe(true)
|
||||
})
|
||||
|
||||
test(`nux is completed`, () => {
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: true,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: undefined,
|
||||
})
|
||||
|
||||
expect(completed).toBe(true)
|
||||
})
|
||||
|
||||
test(`initial state, but already completed for device`, () => {
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady: false,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: true,
|
||||
})
|
||||
|
||||
expect(completed).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncCompletedState', () => {
|
||||
describe('!nuxIsReady', () => {
|
||||
test(`!completedForDevice, no-op`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: false,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: false,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test(`completedForDevice, no-op`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: false,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: true,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nuxIsReady', () => {
|
||||
describe(`!nuxIsCompleted`, () => {
|
||||
describe(`!nuxIsOptimisticallyCompleted`, () => {
|
||||
test(`!completedForDevice, no-op`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: false,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test(`completedForDevice, syncs to server`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: true,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Catches the case where we already called `save` to sync device state
|
||||
* to server, thus `nuxIsOptimisticallyCompleted` is true.
|
||||
*/
|
||||
describe(`nuxIsOptimisticallyCompleted`, () => {
|
||||
test(`completedForDevice, no-op`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: false,
|
||||
nuxIsOptimisticallyCompleted: true,
|
||||
completedForDevice: true,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe(`nuxIsCompleted`, () => {
|
||||
test(`!completedForDevice, syncs to device`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: true,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: false,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test(`completedForDevice, no-op`, () => {
|
||||
const save = jest.fn()
|
||||
const setCompletedForDevice = jest.fn()
|
||||
syncCompletedState({
|
||||
nuxIsReady: true,
|
||||
nuxIsCompleted: true,
|
||||
nuxIsOptimisticallyCompleted: false,
|
||||
completedForDevice: true,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
|
||||
expect(save).not.toHaveBeenCalled()
|
||||
expect(setCompletedForDevice).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
import {ID} from '#/components/PolicyUpdateOverlay/updates/202508/config'
|
||||
|
||||
/**
|
||||
* The singulary active update ID. This is configured here to ensure that
|
||||
* the relationship is clear.
|
||||
*/
|
||||
export const ACTIVE_UPDATE_ID = ID
|
||||
@@ -1,68 +0,0 @@
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
import {Provider as PortalProvider} from '#/components/PolicyUpdateOverlay/Portal'
|
||||
import {
|
||||
type PolicyUpdateState,
|
||||
usePolicyUpdateState,
|
||||
} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
|
||||
|
||||
const Context = createContext<{
|
||||
state: PolicyUpdateState
|
||||
setIsReadyToShowOverlay: () => void
|
||||
}>({
|
||||
state: {
|
||||
completed: true,
|
||||
complete: () => {},
|
||||
},
|
||||
/**
|
||||
* Although our data will be ready to go when the app shell mounts, we don't
|
||||
* want to show the overlay until we actually render it, which happens after
|
||||
* sigin/signup/onboarding in `createNativeStackNavigatorWithAuth`.
|
||||
*/
|
||||
setIsReadyToShowOverlay: () => {},
|
||||
})
|
||||
Context.displayName = 'PolicyUpdateOverlayContext'
|
||||
|
||||
export function usePolicyUpdateContext() {
|
||||
const context = useContext(Context)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'usePolicyUpdateContext must be used within a PolicyUpdateProvider',
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function Provider({children}: {children?: ReactNode}) {
|
||||
const {hasSession} = useSession()
|
||||
const [isReadyToShowOverlay, setIsReadyToShowOverlay] = useState(false)
|
||||
const state = usePolicyUpdateState({
|
||||
// only enable the policy update overlay in non-test environments
|
||||
enabled:
|
||||
isReadyToShowOverlay && hasSession && process.env.NODE_ENV !== 'test',
|
||||
})
|
||||
|
||||
const ctx = useMemo(
|
||||
() => ({
|
||||
state,
|
||||
setIsReadyToShowOverlay() {
|
||||
if (isReadyToShowOverlay) return
|
||||
setIsReadyToShowOverlay(true)
|
||||
},
|
||||
}),
|
||||
[state, isReadyToShowOverlay, setIsReadyToShowOverlay],
|
||||
)
|
||||
|
||||
return (
|
||||
<PortalProvider>
|
||||
<Context.Provider value={ctx}>{children}</Context.Provider>
|
||||
</PortalProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import {useEffect} from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
||||
import {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
|
||||
import {Portal} from '#/components/PolicyUpdateOverlay/Portal'
|
||||
import {Content} from '#/components/PolicyUpdateOverlay/updates/202508'
|
||||
|
||||
export {Provider} from '#/components/PolicyUpdateOverlay/context'
|
||||
export {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
|
||||
export {Outlet} from '#/components/PolicyUpdateOverlay/Portal'
|
||||
|
||||
export function PolicyUpdateOverlay() {
|
||||
const {state, setIsReadyToShowOverlay} = usePolicyUpdateContext()
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Tell the context that we are ready to show the overlay.
|
||||
*/
|
||||
setIsReadyToShowOverlay()
|
||||
}, [setIsReadyToShowOverlay])
|
||||
|
||||
/*
|
||||
* See `window.clearNux` example in `/state/queries/nuxs` for a way to clear
|
||||
* NUX state for local testing and debugging.
|
||||
*/
|
||||
|
||||
if (state.completed) return null
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<FullWindowOverlay>
|
||||
<View
|
||||
style={[
|
||||
a.fixed,
|
||||
a.inset_0,
|
||||
// setting a zIndex when using FullWindowOverlay on iOS
|
||||
// means the taps pass straight through to the underlying content (???)
|
||||
// so don't set it on iOS. FullWindowOverlay already does the job.
|
||||
!isIOS && {zIndex: 9999},
|
||||
]}>
|
||||
<Content state={state} />
|
||||
</View>
|
||||
</FullWindowOverlay>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import {Logger} from '#/logger'
|
||||
|
||||
export const logger = Logger.create(Logger.Context.PolicyUpdate)
|
||||
@@ -1,7 +0,0 @@
|
||||
/*
|
||||
* Keep this file separate to avoid import issues.
|
||||
*/
|
||||
|
||||
import {Nux} from '#/state/queries/nuxs'
|
||||
|
||||
export const ID = Nux.PolicyUpdate202508
|
||||
@@ -1,190 +0,0 @@
|
||||
import {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {InlineLinkText, Link} from '#/components/Link'
|
||||
import {Badge} from '#/components/PolicyUpdateOverlay/Badge'
|
||||
import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay'
|
||||
import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function Content({state}: {state: PolicyUpdateState}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {screenReaderEnabled} = useA11y()
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
state.complete()
|
||||
}, [state])
|
||||
|
||||
const linkStyle = [a.text_md]
|
||||
const links = {
|
||||
terms: {
|
||||
overridePresentation: false,
|
||||
to: `https://bsky.social/about/support/tos`,
|
||||
label: _(msg`Terms of Service`),
|
||||
},
|
||||
privacy: {
|
||||
overridePresentation: false,
|
||||
to: `https://bsky.social/about/support/privacy-policy`,
|
||||
label: _(msg`Privacy Policy`),
|
||||
},
|
||||
copyright: {
|
||||
overridePresentation: false,
|
||||
to: `https://bsky.social/about/support/copyright`,
|
||||
label: _(msg`Copyright Policy`),
|
||||
},
|
||||
guidelines: {
|
||||
overridePresentation: false,
|
||||
to: `https://bsky.social/about/support/community-guidelines`,
|
||||
label: _(msg`Community Guidelines`),
|
||||
},
|
||||
blog: {
|
||||
overridePresentation: false,
|
||||
to: `https://bsky.social/about/blog/08-14-2025-updated-terms-and-policies`,
|
||||
label: _(msg`Our blog post`),
|
||||
},
|
||||
}
|
||||
const linkButtonStyles = {
|
||||
overridePresentation: false,
|
||||
color: 'secondary',
|
||||
size: 'small',
|
||||
} as const
|
||||
|
||||
const label = isAndroid
|
||||
? _(
|
||||
msg`We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 15th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 15th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post.`,
|
||||
)
|
||||
: _(msg`We're updating our policies`)
|
||||
|
||||
return (
|
||||
<Overlay label={label}>
|
||||
<View style={[a.align_start, a.gap_xl]}>
|
||||
<Badge />
|
||||
|
||||
{screenReaderEnabled ? (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text emoji style={[a.text_2xl, a.font_bold, a.leading_snug]}>
|
||||
<Trans>Hey there 👋</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
We’re updating our Terms of Service, Privacy Policy, and
|
||||
Copyright Policy, effective September 15th, 2025.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
We're also updating our Community Guidelines, and we want your
|
||||
input! These new guidelines will take effect on October 15th,
|
||||
2025.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
Learn more about these changes and how to share your thoughts
|
||||
with us by reading our blog post.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Link {...links.terms} {...linkButtonStyles}>
|
||||
<ButtonText>
|
||||
<Trans>Terms of Service</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
<Link {...links.privacy} {...linkButtonStyles}>
|
||||
<ButtonText>
|
||||
<Trans>Privacy Policy</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
<Link {...links.copyright} {...linkButtonStyles}>
|
||||
<ButtonText>
|
||||
<Trans>Copyright Policy</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
<Link {...links.blog} {...linkButtonStyles}>
|
||||
<ButtonText>
|
||||
<Trans>Read our blog post</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text emoji style={[a.text_2xl, a.font_bold, a.leading_snug]}>
|
||||
<Trans>Hey there 👋</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
We’re updating our{' '}
|
||||
<InlineLinkText {...links.terms} style={linkStyle}>
|
||||
Terms of Service
|
||||
</InlineLinkText>
|
||||
,{' '}
|
||||
<InlineLinkText {...links.privacy} style={linkStyle}>
|
||||
Privacy Policy
|
||||
</InlineLinkText>
|
||||
, and{' '}
|
||||
<InlineLinkText {...links.copyright} style={linkStyle}>
|
||||
Copyright Policy
|
||||
</InlineLinkText>
|
||||
, effective September 15th, 2025.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
We're also updating our{' '}
|
||||
<InlineLinkText {...links.guidelines} style={linkStyle}>
|
||||
Community Guidelines
|
||||
</InlineLinkText>
|
||||
, and we want your input! These new guidelines will take effect
|
||||
on October 15th, 2025.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text style={[a.leading_snug, a.text_md]}>
|
||||
<Trans>
|
||||
Learn more about these changes and how to share your thoughts
|
||||
with us by{' '}
|
||||
<InlineLinkText {...links.blog} style={linkStyle}>
|
||||
reading our blog post.
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[a.w_full, a.gap_md]}>
|
||||
<Button
|
||||
label={_(msg`Continue`)}
|
||||
accessibilityHint={_(
|
||||
msg`Tap to acknowledge that you understand and agree to these updates and continue using Bluesky`,
|
||||
)}
|
||||
color="primary"
|
||||
size="large"
|
||||
onPress={handleClose}>
|
||||
<ButtonText>
|
||||
<Trans>Continue</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.leading_snug,
|
||||
a.text_sm,
|
||||
a.italic,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
By clicking "Continue" you acknowledge that you understand and
|
||||
agree to these updates.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Overlay>
|
||||
)
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import {useMemo} from 'react'
|
||||
|
||||
import {useNux, useSaveNux} from '#/state/queries/nuxs'
|
||||
import {ACTIVE_UPDATE_ID} from '#/components/PolicyUpdateOverlay/config'
|
||||
import {logger} from '#/components/PolicyUpdateOverlay/logger'
|
||||
import {IS_DEV} from '#/env'
|
||||
import {device, useStorage} from '#/storage'
|
||||
|
||||
export type PolicyUpdateState = {
|
||||
completed: boolean
|
||||
complete: () => void
|
||||
}
|
||||
|
||||
export function usePolicyUpdateState({
|
||||
enabled,
|
||||
}: {
|
||||
/**
|
||||
* Used to skip the policy update overlay until we're actually ready to
|
||||
* show it.
|
||||
*/
|
||||
enabled: boolean
|
||||
}) {
|
||||
const nux = useNux(ACTIVE_UPDATE_ID)
|
||||
const {mutate: save, variables} = useSaveNux()
|
||||
const deviceStorage = useStorage(device, [ACTIVE_UPDATE_ID])
|
||||
const debugOverride =
|
||||
!!useStorage(device, ['policyUpdateDebugOverride'])[0] && IS_DEV
|
||||
|
||||
return useMemo(() => {
|
||||
/**
|
||||
* If not enabled, then just return a completed state so the app functions
|
||||
* as normal.
|
||||
*/
|
||||
if (!enabled) {
|
||||
return {
|
||||
completed: true,
|
||||
complete() {},
|
||||
}
|
||||
}
|
||||
|
||||
const nuxIsReady = nux.status === 'ready'
|
||||
const nuxIsCompleted = nux.nux?.completed === true
|
||||
const nuxIsOptimisticallyCompleted = !!variables?.completed
|
||||
const [completedForDevice, setCompletedForDevice] = deviceStorage
|
||||
|
||||
const completed = computeCompletedState({
|
||||
nuxIsReady,
|
||||
nuxIsCompleted,
|
||||
nuxIsOptimisticallyCompleted,
|
||||
completedForDevice,
|
||||
})
|
||||
|
||||
logger.debug(`state`, {
|
||||
completed,
|
||||
nux,
|
||||
completedForDevice,
|
||||
})
|
||||
|
||||
if (!debugOverride) {
|
||||
syncCompletedState({
|
||||
nuxIsReady,
|
||||
nuxIsCompleted,
|
||||
nuxIsOptimisticallyCompleted,
|
||||
completedForDevice,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
completed,
|
||||
complete() {
|
||||
logger.debug(`user completed`)
|
||||
save({
|
||||
id: ACTIVE_UPDATE_ID,
|
||||
completed: true,
|
||||
data: undefined,
|
||||
})
|
||||
setCompletedForDevice(true)
|
||||
},
|
||||
}
|
||||
}, [enabled, nux, save, variables, deviceStorage, debugOverride])
|
||||
}
|
||||
|
||||
export function computeCompletedState({
|
||||
nuxIsReady,
|
||||
nuxIsCompleted,
|
||||
nuxIsOptimisticallyCompleted,
|
||||
completedForDevice,
|
||||
}: {
|
||||
nuxIsReady: boolean
|
||||
nuxIsCompleted: boolean
|
||||
nuxIsOptimisticallyCompleted: boolean
|
||||
completedForDevice: boolean | undefined
|
||||
}): boolean {
|
||||
/**
|
||||
* Assume completed to prevent flash
|
||||
*/
|
||||
let completed = true
|
||||
|
||||
/**
|
||||
* Prefer server state, if available
|
||||
*/
|
||||
if (nuxIsReady) {
|
||||
completed = nuxIsCompleted
|
||||
}
|
||||
|
||||
/**
|
||||
* Override with optimistic state or device state
|
||||
*/
|
||||
if (nuxIsOptimisticallyCompleted || !!completedForDevice) {
|
||||
completed = true
|
||||
}
|
||||
|
||||
return completed
|
||||
}
|
||||
|
||||
export function syncCompletedState({
|
||||
nuxIsReady,
|
||||
nuxIsCompleted,
|
||||
nuxIsOptimisticallyCompleted,
|
||||
completedForDevice,
|
||||
save,
|
||||
setCompletedForDevice,
|
||||
}: {
|
||||
nuxIsReady: boolean
|
||||
nuxIsCompleted: boolean
|
||||
nuxIsOptimisticallyCompleted: boolean
|
||||
completedForDevice: boolean | undefined
|
||||
save: ReturnType<typeof useSaveNux>['mutate']
|
||||
setCompletedForDevice: (value: boolean) => void
|
||||
}) {
|
||||
/*
|
||||
* Sync device state to server state for this account
|
||||
*/
|
||||
if (
|
||||
nuxIsReady &&
|
||||
!nuxIsCompleted &&
|
||||
!nuxIsOptimisticallyCompleted &&
|
||||
!!completedForDevice
|
||||
) {
|
||||
logger.debug(`syncing device state to server state`)
|
||||
save({
|
||||
id: ACTIVE_UPDATE_ID,
|
||||
completed: true,
|
||||
data: undefined,
|
||||
})
|
||||
} else if (nuxIsReady && nuxIsCompleted && !completedForDevice) {
|
||||
logger.debug(`syncing server state to device state`)
|
||||
/*
|
||||
* Sync server state to device state
|
||||
*/
|
||||
setCompletedForDevice(true)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import {useCallback} from 'react'
|
||||
|
||||
import {ACTIVE_UPDATE_ID} from '#/components/PolicyUpdateOverlay/config'
|
||||
import {logger} from '#/components/PolicyUpdateOverlay/logger'
|
||||
import {device, useStorage} from '#/storage'
|
||||
|
||||
/*
|
||||
* Marks the active policy update as completed in device storage.
|
||||
* `usePolicyUpdateState` will react to this and replicate this status in the
|
||||
* server NUX state for this account.
|
||||
*/
|
||||
export function usePreemptivelyCompleteActivePolicyUpdate() {
|
||||
const [_completedForDevice, setCompletedForDevice] = useStorage(device, [
|
||||
ACTIVE_UPDATE_ID,
|
||||
])
|
||||
|
||||
return useCallback(() => {
|
||||
logger.debug(`preemptively completing active policy update`)
|
||||
setCompletedForDevice(true)
|
||||
}, [setCompletedForDevice])
|
||||
}
|
||||
@@ -28,7 +28,6 @@ export function createPortalGroup() {
|
||||
append: () => {},
|
||||
remove: () => {},
|
||||
})
|
||||
Context.displayName = 'PortalContext'
|
||||
|
||||
function Provider(props: React.PropsWithChildren<{}>) {
|
||||
const map = useRef<ComponentMap>({})
|
||||
|
||||
@@ -15,7 +15,6 @@ const Context = React.createContext<{
|
||||
setActiveView: (viewId: string) => void
|
||||
sendViewPosition: (viewId: string, y: number) => void
|
||||
} | null>(null)
|
||||
Context.displayName = 'ActiveVideoWebContext'
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
if (!isWeb) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useImperativeHandle, useRef, useState} from 'react'
|
||||
import React, {useRef} from 'react'
|
||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||
@@ -17,88 +17,91 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {TimeIndicator} from './TimeIndicator'
|
||||
|
||||
export function VideoEmbedInnerNative({
|
||||
ref,
|
||||
embed,
|
||||
setStatus,
|
||||
setIsLoading,
|
||||
setIsActive,
|
||||
}: {
|
||||
ref: React.Ref<{togglePlayback: () => void}>
|
||||
embed: AppBskyEmbedVideo.View
|
||||
setStatus: (status: 'playing' | 'paused') => void
|
||||
setIsLoading: (isLoading: boolean) => void
|
||||
setIsActive: (isActive: boolean) => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const videoRef = useRef<BlueskyVideoView>(null)
|
||||
const autoplayDisabled = useAutoplayDisabled()
|
||||
const isWithinMessage = useIsWithinMessage()
|
||||
const [muted, setMuted] = useVideoMuteState()
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [timeRemaining, setTimeRemaining] = useState(0)
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
togglePlayback: () => {
|
||||
videoRef.current?.togglePlayback()
|
||||
export const VideoEmbedInnerNative = React.forwardRef(
|
||||
function VideoEmbedInnerNative(
|
||||
{
|
||||
embed,
|
||||
setStatus,
|
||||
setIsLoading,
|
||||
setIsActive,
|
||||
}: {
|
||||
embed: AppBskyEmbedVideo.View
|
||||
setStatus: (status: 'playing' | 'paused') => void
|
||||
setIsLoading: (isLoading: boolean) => void
|
||||
setIsActive: (isActive: boolean) => void
|
||||
},
|
||||
}))
|
||||
ref: React.Ref<{togglePlayback: () => void}>,
|
||||
) {
|
||||
const {_} = useLingui()
|
||||
const videoRef = useRef<BlueskyVideoView>(null)
|
||||
const autoplayDisabled = useAutoplayDisabled()
|
||||
const isWithinMessage = useIsWithinMessage()
|
||||
const [muted, setMuted] = useVideoMuteState()
|
||||
|
||||
if (error) {
|
||||
throw new Error(error)
|
||||
}
|
||||
const [isPlaying, setIsPlaying] = React.useState(false)
|
||||
const [timeRemaining, setTimeRemaining] = React.useState(0)
|
||||
const [error, setError] = React.useState<string>()
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1, a.relative]}>
|
||||
<BlueskyVideoView
|
||||
url={embed.playlist}
|
||||
autoplay={!autoplayDisabled && !isWithinMessage}
|
||||
beginMuted={autoplayDisabled ? false : muted}
|
||||
style={[a.rounded_sm]}
|
||||
onActiveChange={e => {
|
||||
setIsActive(e.nativeEvent.isActive)
|
||||
}}
|
||||
onLoadingChange={e => {
|
||||
setIsLoading(e.nativeEvent.isLoading)
|
||||
}}
|
||||
onMutedChange={e => {
|
||||
setMuted(e.nativeEvent.isMuted)
|
||||
}}
|
||||
onStatusChange={e => {
|
||||
setStatus(e.nativeEvent.status)
|
||||
setIsPlaying(e.nativeEvent.status === 'playing')
|
||||
}}
|
||||
onTimeRemainingChange={e => {
|
||||
setTimeRemaining(e.nativeEvent.timeRemaining)
|
||||
}}
|
||||
onError={e => {
|
||||
setError(e.nativeEvent.error)
|
||||
}}
|
||||
ref={videoRef}
|
||||
accessibilityLabel={
|
||||
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
|
||||
}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
<VideoControls
|
||||
enterFullscreen={() => {
|
||||
videoRef.current?.enterFullscreen(true)
|
||||
}}
|
||||
toggleMuted={() => {
|
||||
videoRef.current?.toggleMuted()
|
||||
}}
|
||||
togglePlayback={() => {
|
||||
videoRef.current?.togglePlayback()
|
||||
}}
|
||||
isPlaying={isPlaying}
|
||||
timeRemaining={timeRemaining}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
togglePlayback: () => {
|
||||
videoRef.current?.togglePlayback()
|
||||
},
|
||||
}))
|
||||
|
||||
if (error) {
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1, a.relative]}>
|
||||
<BlueskyVideoView
|
||||
url={embed.playlist}
|
||||
autoplay={!autoplayDisabled && !isWithinMessage}
|
||||
beginMuted={autoplayDisabled ? false : muted}
|
||||
style={[a.rounded_sm]}
|
||||
onActiveChange={e => {
|
||||
setIsActive(e.nativeEvent.isActive)
|
||||
}}
|
||||
onLoadingChange={e => {
|
||||
setIsLoading(e.nativeEvent.isLoading)
|
||||
}}
|
||||
onMutedChange={e => {
|
||||
setMuted(e.nativeEvent.isMuted)
|
||||
}}
|
||||
onStatusChange={e => {
|
||||
setStatus(e.nativeEvent.status)
|
||||
setIsPlaying(e.nativeEvent.status === 'playing')
|
||||
}}
|
||||
onTimeRemainingChange={e => {
|
||||
setTimeRemaining(e.nativeEvent.timeRemaining)
|
||||
}}
|
||||
onError={e => {
|
||||
setError(e.nativeEvent.error)
|
||||
}}
|
||||
ref={videoRef}
|
||||
accessibilityLabel={
|
||||
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
|
||||
}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
<VideoControls
|
||||
enterFullscreen={() => {
|
||||
videoRef.current?.enterFullscreen(true)
|
||||
}}
|
||||
toggleMuted={() => {
|
||||
videoRef.current?.toggleMuted()
|
||||
}}
|
||||
togglePlayback={() => {
|
||||
videoRef.current?.togglePlayback()
|
||||
}}
|
||||
isPlaying={isPlaying}
|
||||
timeRemaining={timeRemaining}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function VideoControls({
|
||||
enterFullscreen,
|
||||
|
||||
@@ -224,19 +224,15 @@ function useHLS({
|
||||
throw new HLSUnsupportedError()
|
||||
}
|
||||
|
||||
const latestEstimate = BandwidthEstimate.get()
|
||||
const hls = new Hls({
|
||||
maxMaxBufferLength: 10, // only load 10s ahead
|
||||
// note: the amount buffered is affected by both maxBufferLength and maxBufferSize
|
||||
// it will buffer until it is greater than *both* of those values
|
||||
// so we use maxMaxBufferLength to set the actual maximum amount of buffering instead
|
||||
startLevel:
|
||||
latestEstimate === undefined ? -1 : Hls.DefaultConfig.startLevel,
|
||||
// the '-1' value makes a test request to estimate bandwidth and quality level
|
||||
// before showing the first fragment
|
||||
})
|
||||
hlsRef.current = hls
|
||||
|
||||
const latestEstimate = BandwidthEstimate.get()
|
||||
if (latestEstimate !== undefined) {
|
||||
hls.bandwidthEstimate = latestEstimate
|
||||
}
|
||||
|
||||
@@ -146,8 +146,6 @@ export function Scrubber({
|
||||
const progress = scrubberActive ? seekPosition : currentTime
|
||||
const progressPercent = (progress / duration) * 100
|
||||
|
||||
if (duration < 3) return null
|
||||
|
||||
return (
|
||||
<View
|
||||
testID="scrubber"
|
||||
|
||||
@@ -373,15 +373,13 @@ export function Controls({
|
||||
onPress={onPressPlayPause}
|
||||
/>
|
||||
<View style={a.flex_1} />
|
||||
{Math.round(duration) > 0 && (
|
||||
<Text
|
||||
style={[
|
||||
a.px_xs,
|
||||
{color: t.palette.white, fontVariant: ['tabular-nums']},
|
||||
]}>
|
||||
{formatTime(currentTime)} / {formatTime(duration)}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
a.px_xs,
|
||||
{color: t.palette.white, fontVariant: ['tabular-nums']},
|
||||
]}>
|
||||
{formatTime(currentTime)} / {formatTime(duration)}
|
||||
</Text>
|
||||
{hasSubtitleTrack && (
|
||||
<ControlButton
|
||||
active={subtitlesEnabled}
|
||||
|
||||
@@ -8,7 +8,6 @@ const Context = React.createContext<{
|
||||
volume: number
|
||||
setVolume: React.Dispatch<React.SetStateAction<number>>
|
||||
} | null>(null)
|
||||
Context.displayName = 'VideoVolumeContext'
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [muted, setMuted] = React.useState(true)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useRef, useState} from 'react'
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {ActivityIndicator, View} from 'react-native'
|
||||
import {ImageBackground} from 'expo-image'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
@@ -81,13 +81,13 @@ export function VideoEmbed({embed, crop}: Props) {
|
||||
|
||||
function InnerWrapper({embed}: Props) {
|
||||
const {_} = useLingui()
|
||||
const ref = useRef<{togglePlayback: () => void}>(null)
|
||||
const ref = React.useRef<{togglePlayback: () => void}>(null)
|
||||
|
||||
const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>(
|
||||
const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>(
|
||||
'pending',
|
||||
)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isActive, setIsActive] = useState(false)
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [isActive, setIsActive] = React.useState(false)
|
||||
const showSpinner = useThrottledValue(isActive && isLoading, 100)
|
||||
|
||||
const showOverlay =
|
||||
@@ -96,9 +96,11 @@ function InnerWrapper({embed}: Props) {
|
||||
(status === 'paused' && !isActive) ||
|
||||
status === 'pending'
|
||||
|
||||
if (!isActive && status !== 'pending') {
|
||||
setStatus('pending')
|
||||
}
|
||||
React.useEffect(() => {
|
||||
if (!isActive && status !== 'pending') {
|
||||
setStatus('pending')
|
||||
}
|
||||
}, [isActive, status])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -129,7 +131,8 @@ function InnerWrapper({embed}: Props) {
|
||||
onPress={() => {
|
||||
ref.current?.togglePlayback()
|
||||
}}
|
||||
label={_(msg`Play video`)}>
|
||||
label={_(msg`Play video`)}
|
||||
color="secondary">
|
||||
{showSpinner ? (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
@@ -90,7 +83,9 @@ export function VideoEmbed({
|
||||
style={{display: 'flex', flex: 1, cursor: 'default'}}
|
||||
onClick={evt => evt.stopPropagation()}>
|
||||
<ErrorBoundary renderError={renderError} key={key}>
|
||||
<OnlyNearScreen>
|
||||
<ViewportObserver
|
||||
sendPosition={sendPosition}
|
||||
isAnyViewActive={currentActiveView !== null}>
|
||||
<VideoEmbedInnerWeb
|
||||
embed={embed}
|
||||
active={active}
|
||||
@@ -98,40 +93,31 @@ export function VideoEmbed({
|
||||
onScreen={onScreen}
|
||||
lastKnownTime={lastKnownTime}
|
||||
/>
|
||||
</OnlyNearScreen>
|
||||
</ViewportObserver>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[a.pt_xs]}>
|
||||
<ViewportObserver
|
||||
sendPosition={sendPosition}
|
||||
isAnyViewActive={currentActiveView !== null}>
|
||||
{cropDisabled ? (
|
||||
<View style={[a.w_full, a.overflow_hidden, {aspectRatio: max ?? 1}]}>
|
||||
{contents}
|
||||
</View>
|
||||
) : (
|
||||
<ConstrainedImage
|
||||
fullBleed={crop === 'square'}
|
||||
aspectRatio={constrained || 1}>
|
||||
{contents}
|
||||
</ConstrainedImage>
|
||||
)}
|
||||
</ViewportObserver>
|
||||
{cropDisabled ? (
|
||||
<View style={[a.w_full, a.overflow_hidden, {aspectRatio: max ?? 1}]}>
|
||||
{contents}
|
||||
</View>
|
||||
) : (
|
||||
<ConstrainedImage
|
||||
fullBleed={crop === 'square'}
|
||||
aspectRatio={constrained || 1}>
|
||||
{contents}
|
||||
</ConstrainedImage>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const NearScreenContext = createContext(false)
|
||||
NearScreenContext.displayName = 'VideoNearScreenContext'
|
||||
|
||||
/**
|
||||
* Renders a 100vh tall div and watches it with an IntersectionObserver to
|
||||
* send the position of the div when it's near the screen.
|
||||
*
|
||||
* IMPORTANT: ViewportObserver _must_ not be within a `overflow: hidden` container.
|
||||
*/
|
||||
function ViewportObserver({
|
||||
children,
|
||||
@@ -178,9 +164,7 @@ function ViewportObserver({
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1, a.flex_row]}>
|
||||
<NearScreenContext.Provider value={nearScreen}>
|
||||
{children}
|
||||
</NearScreenContext.Provider>
|
||||
{nearScreen && children}
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
@@ -198,18 +182,6 @@ function ViewportObserver({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Awkward data flow here, but we need to hide the video when it's not near the screen.
|
||||
* But also, ViewportObserver _must_ not be within a `overflow: hidden` container.
|
||||
* So we put it at the top level of the component tree here, then hide the children of
|
||||
* the auto-resizing container.
|
||||
*/
|
||||
export const OnlyNearScreen = ({children}: {children: React.ReactNode}) => {
|
||||
const nearScreen = useContext(NearScreenContext)
|
||||
|
||||
return nearScreen ? children : null
|
||||
}
|
||||
|
||||
function VideoError({error, retry}: {error: unknown; retry: () => void}) {
|
||||
const {_} = useLingui()
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import {View} from 'react-native'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
import {UserInfoText} from '#/view/com/util/UserInfoText'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ArrowCornerDownRight_Stroke2_Corner2_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function PostRepliedTo({
|
||||
parentAuthor,
|
||||
isParentBlocked,
|
||||
isParentNotFound,
|
||||
}: {
|
||||
parentAuthor: string | bsky.profile.AnyProfileView | undefined
|
||||
isParentBlocked?: boolean
|
||||
isParentNotFound?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const textStyle = [a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]
|
||||
|
||||
let label
|
||||
if (isParentBlocked) {
|
||||
label = <Trans context="description">Replied to a blocked post</Trans>
|
||||
} else if (isParentNotFound) {
|
||||
label = <Trans context="description">Replied to a post</Trans>
|
||||
} else if (parentAuthor) {
|
||||
const did =
|
||||
typeof parentAuthor === 'string' ? parentAuthor : parentAuthor.did
|
||||
const isMe = currentAccount?.did === did
|
||||
if (isMe) {
|
||||
label = <Trans context="description">Replied to you</Trans>
|
||||
} else {
|
||||
label = (
|
||||
<Trans context="description">
|
||||
Replied to{' '}
|
||||
<ProfileHoverCard did={did}>
|
||||
<UserInfoText did={did} attr="displayName" style={textStyle} />
|
||||
</ProfileHoverCard>
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!label) {
|
||||
// Should not happen.
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.flex_row, a.align_center, a.pb_xs, a.gap_xs]}>
|
||||
<ArrowCornerDownRightIcon
|
||||
size="xs"
|
||||
style={[t.atoms.text_contrast_medium, {top: -1}]}
|
||||
/>
|
||||
<Text style={textStyle}>{label}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
@@ -32,7 +32,8 @@ export function DiscoverDebug({
|
||||
hitSlop={10}
|
||||
style={[
|
||||
a.absolute,
|
||||
{zIndex: 1000, maxWidth: 65, bottom: -4},
|
||||
a.bottom_0,
|
||||
{zIndex: 1000},
|
||||
gtMobile ? a.right_0 : a.left_0,
|
||||
]}
|
||||
onPress={e => {
|
||||
@@ -41,7 +42,6 @@ export function DiscoverDebug({
|
||||
Toast.show(t`Copied to clipboard`, 'clipboard-check')
|
||||
}}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: theme.palette.contrast_400,
|
||||
fontSize: 7,
|
||||
|
||||
@@ -13,7 +13,6 @@ const PostControlContext = createContext<{
|
||||
active?: boolean
|
||||
color?: {color: string}
|
||||
}>({})
|
||||
PostControlContext.displayName = 'PostControlContext'
|
||||
|
||||
// Base button style, which the the other ones extend
|
||||
export function PostControlButton({
|
||||
|
||||
@@ -17,9 +17,9 @@ 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 {useTranslate} from '#/lib/hooks/useTranslate'
|
||||
import {getCurrentRoute} from '#/lib/routes/helpers'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
@@ -82,7 +83,6 @@ 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 = ({
|
||||
@@ -118,7 +118,6 @@ let PostMenuItems = ({
|
||||
const {hidePost} = useHiddenPostsApi()
|
||||
const feedFeedback = useFeedFeedbackContext()
|
||||
const openLink = useOpenLink()
|
||||
const translate = useTranslate()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
const blockPromptControl = useDialogControl()
|
||||
@@ -173,6 +172,11 @@ let PostMenuItems = ({
|
||||
return makeProfileLink(postAuthor, 'post', urip.rkey)
|
||||
}, [postUri, postAuthor])
|
||||
|
||||
const translatorUrl = getTranslatorLink(
|
||||
record.text,
|
||||
langPrefs.primaryLanguage,
|
||||
)
|
||||
|
||||
const onDeletePost = () => {
|
||||
deletePostMutate({uri: postUri}).then(
|
||||
() => {
|
||||
@@ -230,8 +234,8 @@ let PostMenuItems = ({
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||
}
|
||||
|
||||
const onPressTranslate = () => {
|
||||
translate(record.text, langPrefs.primaryLanguage)
|
||||
const onPressTranslate = async () => {
|
||||
await openLink(translatorUrl, true)
|
||||
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
|
||||
@@ -145,7 +145,7 @@ let RepostButtonDialogInner = ({
|
||||
<View style={a.gap_xl}>
|
||||
<View style={a.gap_xs}>
|
||||
<Button
|
||||
style={[a.justify_start, a.px_md, a.gap_sm]}
|
||||
style={[a.justify_start, a.px_md]}
|
||||
label={
|
||||
isReposted
|
||||
? _(msg`Remove repost`)
|
||||
@@ -167,7 +167,7 @@ let RepostButtonDialogInner = ({
|
||||
<Button
|
||||
disabled={embeddingDisabled}
|
||||
testID="quoteBtn"
|
||||
style={[a.justify_start, a.px_md, a.gap_sm]}
|
||||
style={[a.justify_start, a.px_md]}
|
||||
label={
|
||||
embeddingDisabled
|
||||
? _(msg`Quote posts disabled`)
|
||||
|
||||
@@ -62,17 +62,11 @@ export const RepostButton = ({
|
||||
</Menu.Trigger>
|
||||
<Menu.Outer style={{minWidth: 170}}>
|
||||
<Menu.Item
|
||||
label={
|
||||
isReposted
|
||||
? _(msg`Undo repost`)
|
||||
: _(msg({message: `Repost`, context: `action`}))
|
||||
}
|
||||
label={isReposted ? _(msg`Undo repost`) : _(msg`Repost`)}
|
||||
testID="repostDropdownRepostBtn"
|
||||
onPress={onRepost}>
|
||||
<Menu.ItemText>
|
||||
{isReposted
|
||||
? _(msg`Undo repost`)
|
||||
: _(msg({message: `Repost`, context: `action`}))}
|
||||
{isReposted ? _(msg`Undo repost`) : _(msg`Repost`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Repost} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
@@ -4,12 +4,10 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -59,11 +57,7 @@ export function RecentChats({postUri}: {postUri: string}) {
|
||||
member => member.did !== currentAccount?.did,
|
||||
)
|
||||
|
||||
if (
|
||||
!otherMember ||
|
||||
otherMember.handle === 'missing.invalid' ||
|
||||
convo.muted
|
||||
)
|
||||
if (!otherMember || otherMember.handle === 'missing.invalid')
|
||||
return null
|
||||
|
||||
return (
|
||||
@@ -93,7 +87,7 @@ export function RecentChats({postUri}: {postUri: string}) {
|
||||
const WIDTH = 80
|
||||
|
||||
function RecentChatItem({
|
||||
profile: profileUnshadowed,
|
||||
profile,
|
||||
onPress,
|
||||
moderationOpts,
|
||||
}: {
|
||||
@@ -104,8 +98,6 @@ function RecentChatItem({
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
const name = sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
@@ -113,10 +105,6 @@ function RecentChatItem({
|
||||
)
|
||||
const verification = useSimpleVerificationState({profile})
|
||||
|
||||
if (isBlockedOrBlocking(profile) || isMuted(profile)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
|
||||
@@ -20,13 +20,7 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {
|
||||
atoms as a,
|
||||
platform,
|
||||
type TextStyleProp,
|
||||
useTheme,
|
||||
type ViewStyleProp,
|
||||
} from '#/alf'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
@@ -142,14 +136,12 @@ export function Avatar({
|
||||
onPress,
|
||||
disabledPreview,
|
||||
liveOverride,
|
||||
size = 40,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
onPress?: () => void
|
||||
disabledPreview?: boolean
|
||||
liveOverride?: boolean
|
||||
size?: number
|
||||
}) {
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
|
||||
@@ -157,7 +149,7 @@ export function Avatar({
|
||||
|
||||
return disabledPreview ? (
|
||||
<UserAvatar
|
||||
size={size}
|
||||
size={40}
|
||||
avatar={profile.avatar}
|
||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
moderation={moderation.ui('avatar')}
|
||||
@@ -165,7 +157,7 @@ export function Avatar({
|
||||
/>
|
||||
) : (
|
||||
<PreviewableUserAvatar
|
||||
size={size}
|
||||
size={40}
|
||||
profile={profile}
|
||||
moderation={moderation.ui('avatar')}
|
||||
onBeforePress={onPress}
|
||||
@@ -174,7 +166,7 @@ export function Avatar({
|
||||
)
|
||||
}
|
||||
|
||||
export function AvatarPlaceholder({size = 40}: {size?: number}) {
|
||||
export function AvatarPlaceholder() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
@@ -182,8 +174,8 @@ export function AvatarPlaceholder({size = 40}: {size?: number}) {
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -282,7 +274,7 @@ export function Name({
|
||||
)
|
||||
const verification = useSimpleVerificationState({profile})
|
||||
return (
|
||||
<View style={[a.flex_row, a.align_center, a.max_w_full]}>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[
|
||||
@@ -351,32 +343,13 @@ export function NameAndHandlePlaceholder() {
|
||||
)
|
||||
}
|
||||
|
||||
export function NamePlaceholder({style}: ViewStyleProp) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
width: '60%',
|
||||
height: 14,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Description({
|
||||
profile: profileUnshadowed,
|
||||
numberOfLines = 3,
|
||||
style,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
numberOfLines?: number
|
||||
} & TextStyleProp) {
|
||||
}) {
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const rt = useMemo(() => {
|
||||
if (!('description' in profile)) return
|
||||
@@ -396,7 +369,7 @@ export function Description({
|
||||
<View style={[a.pt_xs]}>
|
||||
<RichText
|
||||
value={rt}
|
||||
style={[a.leading_snug, style]}
|
||||
style={[a.leading_snug]}
|
||||
numberOfLines={numberOfLines}
|
||||
disableLinks
|
||||
/>
|
||||
@@ -513,19 +486,12 @@ export function FollowButtonInner({
|
||||
comment: 'User is following this account, click to unfollow',
|
||||
}),
|
||||
)
|
||||
const followLabel = profile.viewer?.followedBy
|
||||
? _(
|
||||
msg({
|
||||
message: 'Follow back',
|
||||
comment: 'User is not following this account, click to follow back',
|
||||
}),
|
||||
)
|
||||
: _(
|
||||
msg({
|
||||
message: 'Follow',
|
||||
comment: 'User is not following this account, click to follow',
|
||||
}),
|
||||
)
|
||||
const followLabel = _(
|
||||
msg({
|
||||
message: 'Follow',
|
||||
comment: 'User is not following this account, click to follow',
|
||||
}),
|
||||
)
|
||||
|
||||
if (!profile.viewer) return null
|
||||
if (
|
||||
@@ -568,24 +534,6 @@ export function FollowButtonInner({
|
||||
)
|
||||
}
|
||||
|
||||
export function FollowButtonPlaceholder({style}: ViewStyleProp) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.w_full,
|
||||
{
|
||||
height: 33,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Labels({
|
||||
profile,
|
||||
moderationOpts,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {type ProfileHoverCardProps} from './types'
|
||||
import {ProfileHoverCardProps} from './types'
|
||||
|
||||
export function ProfileHoverCard({children}: ProfileHoverCardProps) {
|
||||
return children
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user