Merge branch 'main' into nav-client-events

This commit is contained in:
Alex Benzer
2026-05-25 21:18:43 -07:00
parent f3d2470766
commit bc731ef0d3
137 changed files with 9143 additions and 6464 deletions
@@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
@@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
@@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
@@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
+30 -40
View File
@@ -28,11 +28,11 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 5
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: 🔧 Setup Node
uses: actions/setup-node@v6
@@ -43,25 +43,21 @@ jobs:
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: 🔨 Setup EAS
- name: ⚙️ Install dependencies
run: pnpm install --frozen-lockfile
- name: 🔨 Setup Expo CLI
uses: expo/expo-github-action@main
with:
expo-version: latest
eas-version: latest
packager: pnpm
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: pnpm add -g eas-cli-local-build-plugin
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: "temurin"
java-version: "17"
- name: ⚙️ Install dependencies
run: pnpm install --frozen-lockfile
- name: 🔤 Compile translations
run: pnpm intl:build 2>&1 | tee i18n.log
@@ -92,7 +88,7 @@ jobs:
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
pnpm use-build-number-with-bump
eas build -p android
pnpm eas build -p android
--profile $PROFILE
--local --output build.aab --non-interactive
@@ -109,7 +105,7 @@ jobs:
- name: 🚀 Upload Production Artifact
id: upload-artifact-production
if: ${{ inputs.profile == 'production' }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
retention-days: 30
compression-level: 6
@@ -119,7 +115,7 @@ jobs:
- name: 🚀 Upload Testflight Artifact
id: upload-artifact-testflight
if: ${{ inputs.profile != 'production' }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
retention-days: 30
compression-level: 6
@@ -132,27 +128,23 @@ jobs:
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
uses: slackapi/slack-github-action@v2.1.1
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{
"text": "Android production build for Google Play Store submission is ready!\n```Artifact: ${{ steps.upload-artifact-production.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
{"text": "Android production build for Google Play Store submission is ready!\n```Artifact: ${{ steps.upload-artifact-production.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
- name: 🔔 Notify Slack of Testflight Build
if: ${{ inputs.profile != 'production' }}
uses: slackapi/slack-github-action@v1.25.0
uses: slackapi/slack-github-action@v2.1.1
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact-testflight.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
{"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact-testflight.outputs.artifact-url }}"}
- name: 🧹 Clear Metro cache
if: ${{ inputs.profile == 'production' }}
@@ -166,14 +158,14 @@ jobs:
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
pnpm use-build-number-with-bump
eas build -p android
pnpm eas build -p android
--profile production-apk
--local --output build.apk --non-interactive
- name: 🚀 Upload Production APK Artifact
id: upload-artifact-production-apk
if: ${{ inputs.profile == 'production' }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
retention-days: 30
compression-level: 6
@@ -182,19 +174,17 @@ jobs:
- name: 🔔 Notify Slack of Production APK Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
uses: slackapi/slack-github-action@v2.1.1
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{
"text": "Android production build for GitHub/Obtanium is ready!\n```Artifact: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
{"text": "Android production build for GitHub/Obtanium is ready!\n```Artifact: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
uses: actions/cache@v5
if: ${{ inputs.profile == 'testflight-android' }}
with:
path: most-recent-testflight-commit.txt
+18 -24
View File
@@ -28,11 +28,11 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 5
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: 🔧 Setup Node
uses: actions/setup-node@v6
@@ -43,20 +43,16 @@ jobs:
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
with:
expo-version: latest
eas-version: latest
packager: pnpm
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: pnpm add -g eas-cli-local-build-plugin
- name: ⚙️ Install dependencies
run: pnpm install --frozen-lockfile
- name: 🔨 Setup Expo CLI
uses: expo/expo-github-action@main
with:
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.4"
@@ -67,7 +63,7 @@ jobs:
version: 1.16.2
- name: 💾 Cache Pods
uses: actions/cache@v4
uses: actions/cache@v5
id: pods-cache
with:
path: ./ios/Pods
@@ -104,7 +100,7 @@ jobs:
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
pnpm use-build-number-with-bump
eas build -p ios
pnpm eas build -p ios
--profile $PROFILE
--local --output build.tar.gz --non-interactive
@@ -144,7 +140,7 @@ jobs:
fi
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa"
run: pnpm eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa"
- name: 🪲 Upload dSYM to Sentry
run: >
@@ -159,19 +155,17 @@ jobs:
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
uses: slackapi/slack-github-action@v2.1.1
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{
"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }}```"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
{"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }}```"}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
uses: actions/cache@v5
if: ${{ inputs.profile == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
+26 -41
View File
@@ -48,7 +48,7 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
@@ -56,7 +56,7 @@ jobs:
if: ${{ github.ref != 'refs/heads/main' }}
run: git fetch origin main:main --depth 100
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: 🔧 Setup Node
uses: actions/setup-node@v6
@@ -90,15 +90,10 @@ jobs:
uses: expo/expo-github-action@main
if: ${{ !steps.fingerprint.outputs.includes-changes }}
with:
expo-version: latest
eas-version: latest
packager: pnpm
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup Expo
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: pnpm add -g eas-cli-local-build-plugin
- name: 🪛 Setup jq
if: ${{ !steps.fingerprint.outputs.includes-changes }}
uses: dcarbone/install-jq-action@v2
@@ -142,7 +137,7 @@ jobs:
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
uses: actions/cache@v5
if: ${{ !steps.fingerprint.outputs.includes-changes }}
with:
path: most-recent-testflight-commit.txt
@@ -172,11 +167,11 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 5
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: 🔧 Setup Node
uses: actions/setup-node@v6
@@ -187,14 +182,10 @@ jobs:
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
with:
expo-version: latest
eas-version: latest
packager: pnpm
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: pnpm add -g eas-cli-local-build-plugin
- name: ⚙️ Install dependencies
run: pnpm install --frozen-lockfile
@@ -208,7 +199,7 @@ jobs:
version: 1.16.2
- name: 💾 Cache Pods
uses: actions/cache@v4
uses: actions/cache@v5
id: pods-cache
with:
path: ./ios/Pods
@@ -240,7 +231,7 @@ jobs:
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
pnpm use-build-number-with-bump
eas build -p ios
pnpm eas build -p ios
--profile testflight
--local --output build.tar.gz --non-interactive
@@ -280,7 +271,7 @@ jobs:
fi
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa"
run: pnpm eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa"
- name: 🪲 Upload dSYM to Sentry
run: >
@@ -291,7 +282,7 @@ jobs:
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
uses: actions/cache@v5
if: ${{ inputs.channel == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
@@ -323,11 +314,11 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 5
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: 🔧 Setup Node
uses: actions/setup-node@v6
@@ -338,15 +329,11 @@ jobs:
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
with:
expo-version: latest
eas-version: latest
packager: pnpm
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: pnpm add -g eas-cli-local-build-plugin
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: "temurin"
java-version: "17"
@@ -379,7 +366,7 @@ jobs:
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
pnpm use-build-number-with-bump
eas build -p android
pnpm eas build -p android
--profile testflight-android
--local --output build.apk --non-interactive
@@ -391,7 +378,7 @@ jobs:
- name: 🚀 Upload Artifact
id: upload-artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
retention-days: 30
compression-level: 0
@@ -399,19 +386,17 @@ jobs:
path: build.apk
- name: 🔔 Notify Slack
uses: slackapi/slack-github-action@v1.25.0
uses: slackapi/slack-github-action@v2.1.1
with:
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
webhook-type: incoming-webhook
payload-templated: true
payload: |
{
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
{"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"}
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
uses: actions/cache@v5
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
with:
path: most-recent-testflight-commit.txt
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 1
+4 -4
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Verify Node version pins match .nvmrc
run: |
set -euo pipefail
@@ -41,7 +41,7 @@ jobs:
v=$(grep -oE '"node":[[:space:]]*"[0-9]+\.[0-9]+\.[0-9]+"' eas.json | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sort -u)
check "eas.json" "$v"
exit $rc
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: Install node
uses: actions/setup-node@v6
with:
@@ -66,8 +66,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
uses: actions/checkout@v5
- uses: pnpm/action-setup@v6
- name: Install node
uses: actions/setup-node@v6
with:
@@ -16,10 +16,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ssh-key: ${{secrets.GH_ACTION_DEPLOY_KEY}}
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: Install node
uses: actions/setup-node@v6
with:
+4 -8
View File
@@ -113,11 +113,11 @@ jobs:
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
ref: ${{ steps.pr-info.outputs.head-sha }}
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: 🔧 Setup Node
uses: actions/setup-node@v6
@@ -143,14 +143,10 @@ jobs:
- name: 🔨 Setup EAS
uses: expo/expo-github-action@main
with:
expo-version: latest
eas-version: latest
packager: pnpm
eas-version: '19.0.5'
packager: 'pnpm --allow-build=dtrace-provider'
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup Expo
run: pnpm add -g eas-cli-local-build-plugin
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
+5 -5
View File
@@ -24,11 +24,11 @@ jobs:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: 🔧 Setup Node
uses: actions/setup-node@v6
@@ -68,7 +68,7 @@ jobs:
- name: ⬇️ Get base stats from cache
id: get-base-stats
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: stats-base.json
key: stats-base-${{ steps.base-commit.outputs.base-commit }}
@@ -110,7 +110,7 @@ jobs:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 100
@@ -118,7 +118,7 @@ jobs:
run: git fetch origin main:main --depth 100
if: github.event_name == 'pull_request'
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: 🔧 Setup Node
uses: actions/setup-node@v6
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
if: github.repository == 'bluesky-social/social-app'
steps:
- name: Checkout public repo
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Generate GitHub App Token
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out PR HEAD
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
@@ -21,7 +21,7 @@ jobs:
BASE_REF: ${{ github.base_ref }}
run: git fetch origin $BASE_REF --depth=1
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
- name: Install node
uses: actions/setup-node@v6
-3
View File
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm exec lint-staged --concurrent false
+4
View File
@@ -5,10 +5,14 @@
!**/*.jsx
!**/*.ts
!**/*.tsx
!**/*.mjs
!**/*.cjs
!*/
!**/*.css
!**/pnpm-workspace.yaml
# More specific ignores go below.
.expo
android
+7 -3
View File
@@ -11,10 +11,14 @@ ENV NVM_DIR=/usr/share/nvm
# Go
ENV GODEBUG="netdns=go"
ENV GOOS="linux"
ENV GOARCH="amd64"
ARG TARGETARCH
ENV GOARCH=${TARGETARCH:-amd64}
ENV CGO_ENABLED=1
ENV GOEXPERIMENT="loopvar"
# CI environment for pnpm
ENV CI=true
COPY . .
#
@@ -29,9 +33,9 @@ RUN mkdir --parents $NVM_DIR && \
RUN \. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION && \
nvm use $NODE_VERSION && \
npm install --global pnpm@11.1.1 yarn && \
npm install --global pnpm@11.1.3 && \
pnpm install --frozen-lockfile && \
cd bskyembed && yarn install --frozen-lockfile && cd .. && \
cd bskyembed && pnpm install --frozen-lockfile && cd .. && \
pnpm intl:build && \
pnpm build-embed
+1
View File
@@ -262,6 +262,7 @@ module.exports = function (_config) {
deploymentTarget: '15.1',
buildReactNativeFromSource: true,
ccacheEnabled: IS_DEV,
cxxLanguageStandard: 'c++23',
extraPods: [
{
name: 'MCEmojiPicker',
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M16.091 3.001c.385-.01 1.07.04 1.51.646q.122.17.186.334c.317-.029.759-.066 1.17-.032.395.034 1.195.163 1.661.9.411.65.308 1.316.107 1.768a3.3 3.3 0 0 1-.416.667c.142.124.302.293.428.507.302.514.316 1.083.182 1.62-.236.944-1.025 1.356-1.504 1.54-.493.19-1.07.294-1.502.371l-.05.009-.1.017c.106.368.084.759-.039 1.13-.34 1.025-1.114 1.513-1.85 1.718-.632.176-1.337.176-1.839.175h-.18c.126.508-.018 1.009-.158 1.348-.52 1.25-1.696 1.717-2.647 1.915s-2.052.204-2.966.21h-.063c-.5.002-.945.005-1.334.027-.345.666-.63 1.407-.962 2.271v.003l-.27.701c-.37.943-1.413 1.4-2.33 1.02-.918-.378-1.364-1.45-.996-2.392.473-1.207.962-2.049 1.615-3.342.175-.348.322-.628.452-.889 0-.786.057-1.341.17-2.045.19-1.183.35-1.639.585-2.718.215-.746.532-1.747 1.03-2.299.285-.317.727-.78 1.392-.812.404-.02.656.315.869.457.618-.86 1.143-2.005 1.82-2.894.542-.713 1.125-1.336 2.067-1.188.485.077 1.176.452 1.176 1.188.18-.205.729-.995.96-1.188.44-.367 1.048-.721 1.826-.743m-.347.953c-.828.023-1.412.747-1.902 1.354-.355.44-.66.82-.975.828-.308.008-.285-.204-.256-.473.042-.383.095-.88-.78-1.02-.726-.114-1.492 1.266-2.157 2.464-.455.82-.864 1.555-1.18 1.667-.482.172-.705-.03-.896-.202-.23-.208-.413-.373-.947.22-.563.623-.947 2.328-1.319 4.186-.047.237.283.39.434.203.856-1.057 2.248-2.51 3.908-3.585 1.526-.988 2.998-1.97 4.664-2.464.245-.073.315.21.08.312-1.878.82-3.491 1.903-5.181 3.348-1.9 1.589-3.696 3.703-4.607 5.427a22 22 0 0 0-1.511 3.127.6.6 0 0 0 .32.771.573.573 0 0 0 .751-.329q.134-.345.262-.68c.39-1.02.756-1.976 1.271-2.845a.21.21 0 0 1 .154-.101c.56-.069 1.227-.073 1.917-.077 1.881-.01 3.936-.023 4.487-1.348.287-.692-.06-.792-.373-.881-.255-.074-.489-.141-.335-.52.268-.655 1.138-.655 2.061-.655 1.075 0 2.22 0 2.563-1.036.147-.443-.115-.618-.347-.772-.194-.129-.366-.244-.262-.492.167-.394.89-.525 1.658-.663.917-.165 1.895-.34 2.057-.99.154-.614-.133-.812-.429-1.017-.18-.126-.364-.255-.454-.481-.11-.279.13-.576.38-.885.296-.365.604-.746.362-1.13-.276-.435-1.096-.358-1.775-.295-.424.04-.793.073-.94-.025-.148-.1-.142-.268-.136-.436.009-.262.017-.521-.566-.505"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M7.638 11.873c.096.796.45 2.419 1.255 3.787.097.191.354.16.419 0l4.636-8.401a.394.394 0 0 0-.097-.51 3.73 3.73 0 0 0-2.575-.89c-3.703.095-3.735 4.71-3.638 6.014m5.086 6.237c3.606-.095 3.735-4.677 3.606-5.95-.064-.828-.418-2.483-1.255-3.915-.065-.127-.29-.127-.355 0l-4.668 8.465c-.064.191-.032.382.097.51a3.73 3.73 0 0 0 2.575.89m3.928-13.365-.193.35c-.129.223-.065.477.129.573C19.163 7.29 20 10.09 20 11.937c0 2.291-1.931 7.637-7.823 7.637-1.159 0-2.157-.159-3.058-.445a.415.415 0 0 0-.483.159l-.258.509c-.096.19-.322.255-.515.16l-.354-.192a.37.37 0 0 1-.16-.509l.224-.414c.097-.19.033-.413-.128-.509C4.869 16.678 4 13.846 4 12.064c0-2.291 1.77-7.637 7.694-7.637 1.224 0 2.254.159 3.155.445a.46.46 0 0 0 .515-.19l.29-.51a.366.366 0 0 1 .483-.127l.386.19c.161.096.226.319.129.51"/></svg>

After

Width:  |  Height:  |  Size: 881 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M4.686 8.752c1.117 0 1.88.517 2.246 1.52q.247.678.249 1.643l-.001.097q-.014.94-.267 1.6l-.074.176c-.394.858-1.123 1.303-2.153 1.303a2.32 2.32 0 0 1-1.408-.455l-.035-.028v1.966a1 1 0 0 1-.008.074c-.029.178-.146.267-.359.267H.854q-.273 0-.335-.216l-.01-.035a1 1 0 0 1-.009-.09v-7.34a.7.7 0 0 1 .02-.149.27.27 0 0 1 .155-.18l.027-.01.005-.002a.5.5 0 0 1 .126-.02h2.066q.04 0 .076.007h.007l.048.013a.27.27 0 0 1 .204.231l.005.028a1 1 0 0 1 .004.07 2 2 0 0 1 .185-.131l.039-.024a2 2 0 0 1 .289-.144q.413-.17.93-.171m5.367.041c.46 0 .958.048 1.655.289l.015.005.021.008.06.021.02.007.018.008.017.007.033.014.024.012.015.008.02.01.003.004.01.005.006.004.01.007.003.003.01.008.005.003.009.008.003.003.009.007.011.012.008.009.004.004.006.008.005.005.005.007.013.023.005.009.004.007.002.003a.4.4 0 0 1 .036.134l.004.05.083 1.389a1 1 0 0 1 0 .083v.01l-.004.03-.007.043-.002.008q0 .006-.004.012v.005l-.005.014-.005.014-.002.003-.005.011-.002.004-.006.01-.002.004-.005.008q0 .003-.003.005l-.003.003-.006.009-.003.004-.018.016-.023.018-.005.003-.01.005h-.002l-.01.006q-.004 0-.008.003l-.007.002-.007.002-.009.003-.007.001-.01.003-.005.001-.013.002H12l-.012.002h-.007l-.013.001h-.048l-.013-.003-.057-.008-.124-.02a9 9 0 0 0-1.272-.112c-.804 0-1.146.265-1.146.857 0 .495.212.857 1.134.857a10 10 0 0 0 1.396-.12c.271-.049.366.06.354.326l-.083 1.388a1 1 0 0 1-.01.093l-.001.008-.003.013-.002.006-.004.012-.018.047-.007.016-.002.002-.006.011-.003.006c-.068.107-.192.153-.358.21a4.5 4.5 0 0 1-1.655.3c-1.24 0-2.135-.377-2.706-.996.306-.61.445-1.365.445-2.219 0-.828-.13-1.562-.418-2.16.574-.6 1.464-.963 2.69-.963m11.419-1.227q.354 0 .354.363v1.073c.386.048.796.116 1.265.21.2.041.327.242.285.446l-.307 1.477a.374.374 0 0 1-.438.293 17 17 0 0 0-.805-.142v.603c0 .724.425.82.71.821.117 0 .33-.024.52-.048.271-.036.39.072.378.338l-.082 1.46c-.012.29-.166.46-.415.52a5.5 5.5 0 0 1-1.24.145c-1.869 0-2.614-.978-2.614-2.982v-.985l-.165.008a.4.4 0 0 1-.262-.085l-.002-.002a.4.4 0 0 1-.04-.039l-.004-.003a.4.4 0 0 1-.05-.071l-.003-.005a.4.4 0 0 1-.023-.052l-.002-.005-.008-.025v-.006a.4.4 0 0 1-.01-.057l-.001-.011v-.006l-.001-.019v-.784c0-.165.07-.322.193-.43.191-.168.493-.435.759-.673.154-.139.296-.268.397-.363l.003-.003.744-.683c.201-.181.438-.278.71-.278zM14.052 7c.249 0 .368.12.368.362v2.52c.375-.199.75-.37 1.143-.512.91-.328 1.852-.47 3.017-.482l-.283.25a1.14 1.14 0 0 0-.385.858v.784q.002.214.083.394c-.294.02-.56.052-.806.096l.01.029c.436 1.35 1.099 3.357 1.099 3.357.082.254-.013.386-.273.386h-2.01a.524.524 0 0 1-.532-.386l-.77-2.338q-.142.09-.293.188v2.174q.002.362-.367.362h-1.665a.95.95 0 0 0 .23-.6l.083-1.39v-.007c.008-.165-.001-.484-.243-.718-.248-.24-.564-.225-.748-.194l-.033.005v-.355l.033.006c.197.035.521.046.77-.208.23-.234.24-.544.233-.713v-.009l-.083-1.388a.93.93 0 0 0-.317-.685 1.3 1.3 0 0 0-.382-.209l-.093-.032-.161-.054V7.362q0-.362.354-.362zM3.919 10.868c-.556 0-.74.423-.74 1.132s.184 1.132.74 1.132.738-.423.738-1.132-.182-1.132-.738-1.132"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12.781 4c1.543 0 2.853.528 3.752 1.661q.132.167.25.347.537.358.963.891C18.62 8 19 9.54 19 11.355c0 1.444-.241 2.715-.777 3.73q.081.057.156.128c.238.224.376.504.463.752l.006.014.004.015c.112.359.188.88-.106 1.362q-.091.15-.205.26.024.154.013.322c-.042.569-.407.95-.721 1.18-.234.169-.568.355-.986.355a1.3 1.3 0 0 1-.627-.157 1.3 1.3 0 0 1-.63.157c-.4 0-.744-.186-.978-.356l-.01-.008c-.258-.193-.571-.503-.68-.952a2 2 0 0 1-.523-.278l-.01-.008c-.16-.12-.34-.284-.48-.497a5 5 0 0 1-.373-.1v1.687c0 .341-.085.773-.423 1.11-.333.331-.756.412-1.089.412H7.705c-.334 0-.752-.086-1.08-.42a1.46 1.46 0 0 1-.394-.836 1.4 1.4 0 0 1-.819-.402C5.084 18.491 5 18.064 5 17.723V5.72c0-.341.084-.767.412-1.102.328-.334.746-.42 1.08-.42h3.32c.27 0 .596.053.886.245A4.85 4.85 0 0 1 12.781 4m0 .929c-.95 0-1.747.277-2.368.772 0-.376-.194-.574-.601-.574h-3.32q-.582 0-.583.594v12.002q0 .594.583.594h3.32c.407 0 .601-.198.601-.594v-3.189c.63.487 1.41.782 2.321.792.005-.147.037-.294.083-.43.068-.215.17-.425.336-.585a.91.91 0 0 1 .742-.248 1 1 0 0 1 .209-.425c.236-.277.582-.354.906-.354.32 0 .667.078.902.356q.049.059.086.122c.579-.868.879-2.089.879-3.644 0-3.406-1.437-5.19-4.096-5.19m2.229 8.819c-.532 0-.744.259-.68.784l.037.194-.175-.092a.9.9 0 0 0-.394-.111c-.266 0-.44.167-.55.526-.166.488.018.774.541.866l.192.027-.137.158c-.376.368-.348.7.083 1.023.165.12.32.183.449.184.211 0 .395-.147.542-.433l.092-.176.092.176c.146.286.32.433.532.433.146 0 .294-.064.459-.184.431-.313.449-.655.082-1.033l-.128-.148.183-.027c.533-.092.697-.378.541-.876-.119-.34-.293-.507-.54-.507a.96.96 0 0 0-.405.11l-.183.084.027-.194q.124-.786-.66-.784m-3.49-5.349c.913 0 1.213.696 1.213 1.858s-.3 1.856-1.213 1.856c-.912 0-1.212-.694-1.212-1.856s.3-1.857 1.213-1.858"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.846 2.047a.275.275 0 0 1 .308 0c.108.073.166.791.283 2.227l.04.499h2.76a3.955 3.955 0 0 1 3.954 3.954v.682h-1.364v-.682a2.59 2.59 0 0 0-2.59-2.59H12.59c.123 1.38.245 2.183.605 2.84a4.37 4.37 0 0 0 1.688 1.708c.808.455 1.845.556 3.918.759l1.14.11c1.294.127 1.942.19 2.014.298a.27.27 0 0 1-.003.304c-.073.107-.722.16-2.018.266l-.743.06v2.79a3.954 3.954 0 0 1-3.955 3.955h-2.762l-.037.467c-.116 1.457-.174 2.185-.283 2.259a.275.275 0 0 1-.308 0c-.108-.073-.166-.802-.282-2.259l-.038-.467H8.691a3.955 3.955 0 0 1-3.955-3.954v-.682H6.1v.682a2.59 2.59 0 0 0 2.59 2.59h2.725c-.124-1.433-.247-2.26-.618-2.932a4.37 4.37 0 0 0-1.72-1.715c-.823-.452-1.878-.538-3.988-.71l-1.023-.084c-1.296-.106-1.945-.16-2.018-.266a.27.27 0 0 1-.003-.304c.072-.108.72-.171 2.015-.297l.676-.066V8.727a3.955 3.955 0 0 1 3.955-3.954h2.832l.04-.5c.117-1.435.175-2.153.283-2.226m5.981 10.55c-1.414.126-2.235.252-2.903.619a4.37 4.37 0 0 0-1.72 1.715c-.372.672-.495 1.499-.62 2.933h2.652a2.59 2.59 0 0 0 2.591-2.591zm-9.136-6.46A2.59 2.59 0 0 0 6.1 8.726v2.627c1.492-.153 2.336-.285 3.017-.669a4.37 4.37 0 0 0 1.688-1.707c.36-.658.483-1.46.605-2.842z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+3
View File
@@ -47,6 +47,9 @@ module.exports = function (api) {
production: {
plugins: ['transform-remove-console'],
},
test: {
plugins: ['@babel/plugin-transform-class-static-block'],
},
},
}
}
+3 -3
View File
@@ -4,13 +4,13 @@ Install dependencies:
```bash
cd bskyembed
yarn
pnpm
```
Run the dev server:
```bash
yarn dev
pnpm dev
```
You can see the embed homepage at http://localhost:5173
@@ -20,7 +20,7 @@ You can see the embed homepage at http://localhost:5173
In another terminal window, run the snippet dev script:
```bash
yarn dev-snippet
pnpm dev-snippet
```
You can then see the testbed page at http://localhost:5173/test
+8 -2
View File
@@ -1,10 +1,12 @@
// @ts-check
import js from '@eslint/js'
import {defineConfig} from 'eslint/config'
import tseslint from 'typescript-eslint'
import simpleImportSort from 'eslint-plugin-simple-import-sort'
import {importX} from 'eslint-plugin-import-x'
import globals from 'globals'
export default tseslint.config(
export default defineConfig(
// Global ignores
{
ignores: ['dist/**', 'node_modules/**'],
@@ -14,7 +16,11 @@ export default tseslint.config(
js.configs.recommended,
// TypeScript rules with type checking
...tseslint.configs.recommendedTypeChecked,
tseslint.configs.recommendedTypeChecked,
// import-x
importX.flatConfigs.recommended,
importX.flatConfigs.typescript,
// Main configuration
{
+12 -8
View File
@@ -7,29 +7,33 @@
"dev-snippet": "tsc --project tsconfig.snippet.json && serve -s dist -p 3000 -n",
"build": "tsc && vite build",
"build-snippet": "tsc --project tsconfig.snippet.json",
"analyze": "ANALYZE=1 vite build",
"lint": "eslint --cache src",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"format": "prettier -w src"
},
"dependencies": {
"@atproto/api": "^0.15.25",
"preact": "^10.4.8"
},
"devDependencies": {
"@preact/preset-vite": "^2.10.2",
"@vitejs/plugin-legacy": "^7.0.0",
"@eslint/js": "^9.18.0",
"@preact/preset-vite": "^2.10.2",
"@vitejs/plugin-legacy": "^8.0.1",
"autoprefixer": "^10.4.19",
"eslint": "^9.18.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-simple-import-sort": "^13.0.0",
"globals": "^15.14.0",
"postcss": "^8.4.38",
"typescript-eslint": "^8.20.0",
"prettier": "^3.8.3",
"serve": "^14.2.5",
"tailwindcss": "^3.4.3",
"terser": "^5.43.1",
"typescript": "^5.8.3",
"vite": "^7.0.4",
"vite-tsconfig-paths": "^5.1.4"
"typescript": "^6.0.3",
"typescript-eslint": "^8.20.0",
"vite": "^8.0.12",
"vite-bundle-analyzer": "^1.3.8"
},
"resolutions": {
"@types/estree": "1.0.6"
+4655
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
allowBuilds:
'@sentry/cli': true
'core-js': true
'esbuild': true
'unrs-resolver': true
minimumReleaseAgeExclude:
- '@atproto/*'
- '@bsky.app/*'
+1
View File
@@ -416,6 +416,7 @@ function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
controls
playsinline
preload="metadata"
// @ts-expect-error https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/video#loading
loading="lazy"
aria-label={content.alt || undefined}
onClickCapture={evt => evt.stopPropagation()}
+3 -5
View File
@@ -1,11 +1,10 @@
{
"compilerOptions": {
"target": "ES5",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"target": "ES2015",
"lib": ["DOM", "ESNext"],
"types": ["vite/client"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
@@ -17,7 +16,6 @@
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"downlevelIteration": true
},
"include": ["src", "snippet", "vite.config.ts", "*.config.cjs", ".eslintrc.cjs"]
"include": ["src", "snippet", "vite.config.ts", "*.config.cjs", "eslint.config.mjs"]
}
+3 -2
View File
@@ -1,8 +1,9 @@
{
"compilerOptions": {
"target": "ES5",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"target": "ES2015",
"lib": ["DOM", "ESNext"],
"strict": true,
"rootDir": "snippet",
"outDir": "dist"
},
"include": ["snippet"]
+8 -3
View File
@@ -1,10 +1,10 @@
import fs from 'node:fs'
import {resolve} from 'node:path'
import preact from '@preact/preset-vite'
import {preact} from '@preact/preset-vite'
import legacy from '@vitejs/plugin-legacy'
import type {Plugin, UserConfig} from 'vite'
import paths from 'vite-tsconfig-paths'
import {analyzer} from 'vite-bundle-analyzer'
/**
* World's hackiest router, for dev only. Serves `/post.html` to requests that start with `/embed/`
@@ -38,11 +38,13 @@ function devOnlyRouter(): Plugin {
const config: UserConfig = {
plugins: [
preact(),
paths(),
legacy({
targets: ['defaults', 'not IE 11'],
}),
devOnlyRouter(),
analyzer({
enabled: process.env.ANALYZE === '1',
}),
],
build: {
assetsDir: 'static',
@@ -53,6 +55,9 @@ const config: UserConfig = {
},
},
},
resolve: {
tsconfigPaths: true,
},
}
export default config
-3516
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"cli": {
"version": ">= 3.8.1",
"version": ">=18",
"promptToConfigurePushNotifications": false,
"appVersionSource": "remote"
},
-10
View File
@@ -59,11 +59,6 @@
"count": 1
}
},
"src/components/Layout/const.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 2
}
},
"src/components/Lists.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -688,11 +683,6 @@
"count": 1
}
},
"src/view/screens/DebugMod.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
}
},
"src/view/shell/createNativeStackNavigatorWithAuth.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
+45 -30
View File
@@ -1,7 +1,7 @@
// @ts-check
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import { defineConfig } from 'eslint/config';
import {defineConfig} from 'eslint/config'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
// @ts-expect-error no types
@@ -184,24 +184,30 @@ export default defineConfig(
* Import linting
*/
'import-x/consistent-type-specifier-style': ['warn', 'prefer-inline'],
'import-x/no-unresolved': ['error', {
/*
* The `postinstall` hook runs `compile-if-needed` locally, but not in
* CI. For CI-sake, ignore this.
*/
ignore: ['^#\/locale\/locales\/.+\/messages'],
}],
'import-x/no-extraneous-dependencies': ['error', {
'whitelist': [
// test files only
'@jest/globals',
// we only use a really simple util from this, and we know it will be present
'expo-modules-core',
// this is a dep for @atproto/api, but we absolutely need them in sync, so just
// rely on the transient version
'@atproto/common-web',
]
}],
'import-x/no-unresolved': [
'error',
{
/*
* The `postinstall` hook runs `compile-if-needed` locally, but not in
* CI. For CI-sake, ignore this.
*/
ignore: ['^#\/locale\/locales\/.+\/messages'],
},
],
'import-x/no-extraneous-dependencies': [
'error',
{
whitelist: [
// test files only
'@jest/globals',
// we only use a really simple util from this, and we know it will be present
'expo-modules-core',
// this is a dep for @atproto/api, but we absolutely need them in sync, so just
// rely on the transient version
'@atproto/common-web',
],
},
],
'import-x/no-nodejs-modules': 'error',
/**
@@ -222,9 +228,12 @@ export default defineConfig(
{prefer: 'type-imports', fixStyle: 'inline-type-imports'},
],
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unused-expressions': ['error', {
allowTernary: true,
}],
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowTernary: true,
},
],
/**
* Maintain previous behavior - these are stricter in typescript-eslint
* v8 `warn` ones are probably worth fixing. `off` ones are a bit too
@@ -251,13 +260,19 @@ export default defineConfig(
'@typescript-eslint/prefer-promise-reject-errors': 'warn',
'@typescript-eslint/await-thenable': 'warn',
"no-restricted-imports": ["error", {
"paths": [{
"name": "react",
"importNames": ["React", "default"],
"message": "React is already in the global type namespace. Use named imports for runtime modules."
}]
}],
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'react',
importNames: ['React', 'default'],
message:
'React is already in the global type namespace. Use named imports for runtime modules.',
},
],
},
],
/**
* Turn off rules that we haven't enforced thus far
@@ -298,7 +313,7 @@ export default defineConfig(
languageOptions: {
globals: {
...globals.jest,
}
},
},
},
)
-4
View File
@@ -17,10 +17,6 @@ jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => {
}
})
jest.mock('@fortawesome/react-native-fontawesome', () => ({
FontAwesomeIcon: '',
}))
jest.mock('react-native-safe-area-context', () => {
const inset = {top: 0, right: 0, bottom: 0, left: 0}
return {
+1 -28
View File
@@ -2,6 +2,7 @@
const {getSentryExpoConfig} = require('@sentry/react-native/metro')
const cfg = getSentryExpoConfig(__dirname)
// inject `.e2e.ts` and `.e2e.tsx` into the sourceExts when running tests
cfg.resolver.sourceExts = process.env.RN_SRC_EXT
? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts)
: cfg.resolver.sourceExts
@@ -16,25 +17,7 @@ if (process.env.BSKY_PROFILE) {
cfg.resolver.assetExts = [...cfg.resolver.assetExts, 'woff2']
// Enabled by default in RN 0.79+, but this breaks Lingui + others
cfg.resolver.unstable_enablePackageExports = false
cfg.resolver.resolveRequest = (context, moduleName, platform) => {
// HACK: manually resolve a few packages that use `exports` in `package.json`.
// A proper solution is to enable `unstable_enablePackageExports` but this needs careful testing.
if (moduleName.startsWith('multiformats/hashes/hasher')) {
return context.resolveRequest(
context,
'multiformats/cjs/src/hashes/hasher',
platform,
)
}
if (moduleName.startsWith('multiformats/cid')) {
return context.resolveRequest(context, 'multiformats/cjs/src/cid', platform)
}
if (moduleName === '@ipld/dag-cbor') {
return context.resolveRequest(context, '@ipld/dag-cbor/src', platform)
}
if (process.env.BSKY_PROFILE) {
if (moduleName.endsWith('ReactNativeRenderer-prod')) {
return context.resolveRequest(
@@ -51,16 +34,6 @@ cfg.transformer.getTransformOptions = async () => ({
transform: {
experimentalImportSupport: true,
inlineRequires: true,
nonInlinedRequires: [
// We can remove this option and rely on the default after
// https://github.com/facebook/metro/pull/1390 is released.
'React',
'react',
'react-compiler-runtime',
'react/jsx-dev-runtime',
'react/jsx-runtime',
'react-native',
],
},
})
+100 -11
View File
@@ -1,6 +1,6 @@
import UserNotifications
import UIKit
import Intents
import UIKit
import UserNotifications
let APP_GROUP = "group.app.bsky"
typealias ContentHandler = (UNNotificationContent) -> Void
@@ -30,11 +30,14 @@ class NotificationService: UNNotificationServiceExtension {
private var contentHandler: ContentHandler?
private var bestAttempt: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
guard let bestAttempt = NSEUtil.createCopy(request.content),
let reason = request.content.userInfo["reason"] as? String
let reason = request.content.userInfo["reason"] as? String
else {
contentHandler(request.content)
return
@@ -44,11 +47,26 @@ class NotificationService: UNNotificationServiceExtension {
if reason == "chat-message" || reason == "chat-reaction" {
mutateWithChatMessage(bestAttempt)
// Only apply the title->body swap for chat-message. For chat-reaction,
// `messageKind` refers to the reacted-to message, so we'd clobber the
// descriptive reaction body with the title for reactions on system
// messages.
if reason == "chat-message" {
mutateChatMessageBody(bestAttempt, userInfo: request.content.userInfo)
}
mutateWithGroupSubtitle(bestAttempt, userInfo: request.content.userInfo)
let finalContent = createCommunicationNotification(
from: bestAttempt,
userInfo: request.content.userInfo
)
contentHandler(finalContent)
} else if reason == "chat-added-to-group"
|| reason == "chat-removed-from-group"
|| reason == "chat-join-request-rejected" {
mutateWithChatMessage(bestAttempt)
mutateWithGroupSubtitle(bestAttempt, userInfo: request.content.userInfo)
mutateWithBadge(bestAttempt)
contentHandler(bestAttempt)
} else {
mutateWithBadge(bestAttempt)
contentHandler(bestAttempt)
@@ -57,7 +75,8 @@ class NotificationService: UNNotificationServiceExtension {
override func serviceExtensionTimeWillExpire() {
guard let contentHandler = self.contentHandler,
let bestAttempt = self.bestAttempt else {
let bestAttempt = self.bestAttempt
else {
return
}
contentHandler(bestAttempt)
@@ -71,7 +90,7 @@ class NotificationService: UNNotificationServiceExtension {
) -> UNNotificationContent {
let senderDisplayName = userInfo["senderDisplayName"] as? String ?? "Unknown"
let convoId = userInfo["convoId"] as? String
var avatarImage: INImage? = nil
var avatarImage: INImage?
if let avatarUrlString = userInfo["senderAvatarUrl"] as? String {
avatarImage = downloadAvatarImage(from: avatarUrlString)
}
@@ -87,17 +106,53 @@ class NotificationService: UNNotificationServiceExtension {
customIdentifier: nil
)
var speakableGroupName: INSpeakableString?
if userInfo["convoKind"] as? String == "group",
let groupName = userInfo["groupName"] as? String,
!groupName.isEmpty {
speakableGroupName = INSpeakableString(spokenPhrase: groupName)
}
let intent = INSendMessageIntent(
recipients: nil,
outgoingMessageType: .outgoingMessageText,
content: content.body,
speakableGroupName: nil,
speakableGroupName: speakableGroupName,
conversationIdentifier: convoId,
serviceName: nil,
sender: sender,
attachments: nil
)
// The push payload omits the full recipient list for group convos. Without
// any signal of multiple recipients, iOS classifies the notification as
// `MessagingDirect` and ignores `speakableGroupName`. Setting
// `recipientCount` on the donation metadata is Apple's documented escape
// hatch for this case, and overrides the (zero-length) recipients array.
if userInfo["convoKind"] as? String == "group",
let groupMemberCount = userInfo["groupMemberCount"] as? Int,
groupMemberCount > 0 {
let metadata = INSendMessageIntentDonationMetadata()
metadata.recipientCount = groupMemberCount
intent.donationMetadata = metadata
}
// For group convos, attach the composite group avatar (rendered by the
// ogcard service) to the `speakableGroupName` parameter so iOS shows it
// alongside the sender on the Communication Notification.
// Disabled: the composite avatar renders poorly at notification size.
// if userInfo["convoKind"] as? String == "group",
// let convoAvatarUrlString = userInfo["convoAvatarUrl"] as? String,
// let groupImage = downloadAvatarImage(from: convoAvatarUrlString) {
// intent.setImage(groupImage, forParameterNamed: \.speakableGroupName)
// }
// Set the group image to the sender avatar instead
if userInfo["convoKind"] as? String == "group",
let avatarImage {
intent.setImage(avatarImage, forParameterNamed: \.speakableGroupName)
}
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
interaction.donate(completion: nil)
@@ -120,13 +175,13 @@ class NotificationService: UNNotificationServiceExtension {
var request = URLRequest(url: url)
request.timeoutInterval = 5
var imageData: Data? = nil
var imageData: Data?
let semaphore = DispatchSemaphore(value: 0)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let task = URLSession.shared.dataTask(with: request) { data, response, _ in
if let data = data,
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
imageData = data
}
semaphore.signal()
@@ -157,6 +212,40 @@ class NotificationService: UNNotificationServiceExtension {
}
}
// For group convos, surface the group name as the notification subtitle.
// The sender's display name is shown as the title (overridden by
// `INSendMessageIntent` for chat-message/chat-reaction).
func mutateWithGroupSubtitle(
_ content: UNMutableNotificationContent,
userInfo: [AnyHashable: Any]
) {
guard userInfo["convoKind"] as? String == "group",
let groupName = userInfo["groupName"] as? String,
!groupName.isEmpty
else {
return
}
content.subtitle = groupName
}
// System messages (`add_member`, `convo_locked`, `edit_group`, etc.) are
// delivered through `chat-message` but have a server-rendered description
// in `title`. iOS overrides the title with the sender name once we apply
// `INSendMessageIntent`, so we move the title text into the body before
// building the intent.
func mutateChatMessageBody(
_ content: UNMutableNotificationContent,
userInfo: [AnyHashable: Any]
) {
guard let messageKind = userInfo["messageKind"] as? String,
messageKind != "message",
!content.title.isEmpty
else {
return
}
content.body = content.title
}
func mutateWithDefaultSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound.default
}
@@ -1,11 +1,12 @@
import ExpoModulesCore
import WebKit
import MCEmojiPicker
class EmojiPickerView: ExpoView, MCEmojiPickerDelegate {
let onEmojiSelected = EventDispatcher()
override func layoutSubviews() {
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
self.addGestureRecognizer(tapGesture)
}
@@ -19,9 +20,19 @@ class EmojiPickerView: ExpoView, MCEmojiPickerDelegate {
let reactRootVC = reactViewController()
emojiPicker.sourceView = self
emojiPicker.delegate = self
emojiPicker.arrowDirection = preferredArrowDirection()
reactRootVC?.present(emojiPicker, animated: true)
}
/// If the trigger sits in the bottom third of the screen there isn't enough
/// room below it for a usable popover, so anchor the picker above instead.
private func preferredArrowDirection() -> MCPickerArrowDirection {
guard let window = self.window else { return .up }
let frameInWindow = self.convert(self.bounds, to: window)
let threshold = window.bounds.height * (2.0 / 3.0)
return frameInWindow.midY >= threshold ? .down : .up
}
func didGetEmoji(emoji: String) {
onEmojiSelected([
"emoji": emoji
+37 -19
View File
@@ -5,7 +5,18 @@
"engines": {
"node": ">=24.15.0"
},
"packageManager": "pnpm@11.1.3+sha512.c85357fe17ca12dd23dd7071822666dfd7e3cb76fe214e3370b5ea2fb34f2a231185509b63e717f3cd0acb38dd3f8d82bcd5e8172400ae678b70ea4fbed0896d",
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "11.1.3",
"onFail": "warn"
},
"runtime": {
"name": "node",
"version": "^24.15.0",
"onFail": "download"
}
},
"expo": {
"autolinking": {
"android": {
@@ -26,7 +37,7 @@
}
},
"scripts": {
"prepare": "is-ci || husky install",
"prepare": "is-ci || husky",
"postinstall": "pnpm intl:compile-if-needed",
"prebuild": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean",
"android": "expo run:android",
@@ -41,7 +52,7 @@
"build-ios": "pnpm use-build-number-with-bump eas build -p ios",
"build-android": "pnpm use-build-number-with-bump eas build -p android",
"build": "pnpm use-build-number-with-bump eas build",
"build-embed": "cd bskyembed && yarn build && yarn build-snippet && cd .. && node ./scripts/post-embed-build.js",
"build-embed": "cd bskyembed && pnpm build && pnpm build-snippet && cd .. && node ./scripts/post-embed-build.js",
"start": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev --minify",
"test": "NODE_ENV=test jest --forceExit --testTimeout=20000 --bail",
@@ -81,19 +92,19 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.19.19",
"@atproto/syntax": "0.5.2",
"@atproto/api": "0.20.5",
"@atproto/syntax": "0.6.1",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.9",
"@bsky.app/alf": "^0.1.14",
"@bsky.app/expo-guess-language": "^0.2.8",
"@bsky.app/expo-image-crop-tool": "^0.5.0",
"@bsky.app/expo-image-crop-tool": "^0.5.1",
"@bsky.app/expo-scroll-edge-effect": "^0.1.4",
"@bsky.app/expo-translate-text": "^0.2.9",
"@bsky.app/react-native-mmkv": "2.12.5",
"@bsky.app/sift": "^0.3.4",
"@bsky.app/tapper": "^0.5.3",
"@bsky.app/video": "0.3.4",
"@bsky.app/sift": "^0.3.8",
"@bsky.app/tapper": "^0.5.7",
"@bsky.app/video": "0.3.6",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
@@ -105,13 +116,9 @@
"@formatjs/intl-locale": "^4.2.13",
"@formatjs/intl-numberformat": "^8.15.6",
"@formatjs/intl-pluralrules": "^5.4.6",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.2",
"@growthbook/growthbook": "^1.6.5",
"@growthbook/growthbook-react": "^1.6.5",
"@ipld/dag-cbor": "^9.2.0",
"@ipld/dag-cbor": "^9.2.7",
"@lingui/core": "^5.9.2",
"@lingui/react": "^5.9.2",
"@miblanchard/react-native-slider": "^2.6.0",
@@ -196,7 +203,7 @@
"lodash.debounce": "^4.0.8",
"lodash.shuffle": "^4.2.0",
"lodash.throttle": "^4.1.1",
"multiformats": "9.9.0",
"multiformats": "^13.4.2",
"nanoid": "^5.0.5",
"normalize-url": "^8.0.0",
"psl": "1.9.0",
@@ -213,7 +220,7 @@
"react-native-date-picker": "^5.0.13",
"react-native-device-attest": "^0.1.6",
"react-native-drawer-layout": "^4.2.3",
"react-native-edge-to-edge": "^1.6.0",
"react-native-edge-to-edge": "^1.8.1",
"react-native-gesture-handler": "~2.28.0",
"react-native-keyboard-controller": "^1.21.7",
"react-native-pager-view": "6.8.0",
@@ -280,7 +287,7 @@
"eslint-plugin-simple-import-sort": "^13.0.0",
"file-loader": "6.2.0",
"globals": "^17.0.0",
"husky": "^8.0.3",
"husky": "^9.1.7",
"is-ci": "^3.0.1",
"jest": "^29.7.0",
"jest-expo": "~54.0.17",
@@ -311,8 +318,19 @@
"transform": {
"\\.[jt]sx?$": "babel-jest"
},
"moduleNameMapper": {
"^multiformats$": "<rootDir>/node_modules/multiformats/dist/src/index.js",
"^multiformats/cid$": "<rootDir>/node_modules/multiformats/dist/src/cid.js",
"^multiformats/bases/base32$": "<rootDir>/node_modules/multiformats/dist/src/bases/base32.js",
"^multiformats/hashes/digest$": "<rootDir>/node_modules/multiformats/dist/src/hashes/digest.js",
"^multiformats/hashes/sha2$": "<rootDir>/node_modules/multiformats/dist/src/hashes/sha2.js",
"^uint8arrays/from-string$": "<rootDir>/node_modules/uint8arrays/dist/src/from-string.js",
"^uint8arrays/to-string$": "<rootDir>/node_modules/uint8arrays/dist/src/to-string.js",
"^unicode-segmenter/grapheme$": "<rootDir>/node_modules/unicode-segmenter/grapheme.cjs",
"^await-lock$": "<rootDir>/node_modules/await-lock/build/AwaitLock.js"
},
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|@discord|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match)"
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|@discord|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match|@atproto/.*|multiformats|uint8arrays|@ipld/.*|cborg|await-lock)"
],
"modulePathIgnorePatterns": [
"__tests__/.*/__mocks__",
+424 -162
View File
@@ -1,3 +1,202 @@
---
lockfileVersion: '9.0'
importers:
.:
configDependencies: {}
packageManagerDependencies:
'@pnpm/exe':
specifier: 11.1.3
version: 11.1.3
pnpm:
specifier: 11.1.3
version: 11.1.3
packages:
'@pnpm/exe@11.1.3':
resolution: {integrity: sha512-J6bSMpZlHVUKuMKtPT/+lrFPpvBiOIgk6HnNC3vmoM/fsMgFhao6ITFwdsUMzdCl2qHf9cnVYA4ZBdsGmVUnpg==}
hasBin: true
'@pnpm/linux-arm64@11.1.3':
resolution: {integrity: sha512-sz3fc0hSqguk2eGe/InelpBD3LP82MzF+8pLzDpYNGuhasGY+VWkuxEo02HczQYRVXTsbpZVepk+Qs2CONc04Q==}
cpu: [arm64]
os: [linux]
'@pnpm/linux-x64@11.1.3':
resolution: {integrity: sha512-UadJh5fJZWa47OtdZTWLKWDj4z5WpZFB5pS2wOh/kfKypUmQyjmbOMClP7/yJGS2ZtrSjRgYjIEWAjndkWsMaA==}
cpu: [x64]
os: [linux]
'@pnpm/linuxstatic-arm64@11.1.3':
resolution: {integrity: sha512-lWmGr96w+VrIRVsEfTXROB3GeQNxrX2Hy32j3USHr6WlqmpH1i7YjavJGpoXeVZbb77fCFjNFAtsJzGXSgy/Og==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/linuxstatic-x64@11.1.3':
resolution: {integrity: sha512-I74GDBOPbr5TXgob3ct4hv6BQtLGdsjGJrII2qNl/e2xYHXekjIWE4Eh7RGN4C7xu+BZYKtvnOOr149yR7KxUw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/macos-arm64@11.1.3':
resolution: {integrity: sha512-nWn155BVa54iNyg4iolVhjMtqumXHPh8ul5CFjQFJXdwgr9MRUgc5EUBML1+7mS8C/7Wcex5HUgn/w3fliHCsQ==}
cpu: [arm64]
os: [darwin]
'@pnpm/win-arm64@11.1.3':
resolution: {integrity: sha512-4u6PQL7/WgwdweC2ZJcdCzhM1koRFG5ofcUSrIVsdyo9ePfGq2D9p1rkZbiLEfIGvl3GKOsfMB5DRTgs8es4AQ==}
cpu: [arm64]
os: [win32]
'@pnpm/win-x64@11.1.3':
resolution: {integrity: sha512-JmhH7ljJ3MWjvWFz/YHbu/27ISCNLZsQb1JG+ib3uHlfUwIJuF6BudXYsbS5Uu0o7xUvC9sPPm+Uho4Or6R3vA==}
cpu: [x64]
os: [win32]
'@reflink/reflink-darwin-arm64@0.1.19':
resolution: {integrity: sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@reflink/reflink-darwin-x64@0.1.19':
resolution: {integrity: sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@reflink/reflink-linux-arm64-gnu@0.1.19':
resolution: {integrity: sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@reflink/reflink-linux-arm64-musl@0.1.19':
resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@reflink/reflink-linux-x64-gnu@0.1.19':
resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@reflink/reflink-linux-x64-musl@0.1.19':
resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@reflink/reflink-win32-arm64-msvc@0.1.19':
resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@reflink/reflink-win32-x64-msvc@0.1.19':
resolution: {integrity: sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@reflink/reflink@0.1.19':
resolution: {integrity: sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==}
engines: {node: '>= 10'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
pnpm@11.1.3:
resolution: {integrity: sha512-yFNX/hfKEt0j3XBxgiZm39fjy3b+IU4zcLXqL7NPKiMRhVCbY+cX880KyzjdP42CvNXoFyQArmeLcOpPvtCJbQ==}
engines: {node: '>=22.13'}
hasBin: true
snapshots:
'@pnpm/exe@11.1.3':
dependencies:
'@reflink/reflink': 0.1.19
detect-libc: 2.1.2
optionalDependencies:
'@pnpm/linux-arm64': 11.1.3
'@pnpm/linux-x64': 11.1.3
'@pnpm/linuxstatic-arm64': 11.1.3
'@pnpm/linuxstatic-x64': 11.1.3
'@pnpm/macos-arm64': 11.1.3
'@pnpm/win-arm64': 11.1.3
'@pnpm/win-x64': 11.1.3
'@pnpm/linux-arm64@11.1.3':
optional: true
'@pnpm/linux-x64@11.1.3':
optional: true
'@pnpm/linuxstatic-arm64@11.1.3':
optional: true
'@pnpm/linuxstatic-x64@11.1.3':
optional: true
'@pnpm/macos-arm64@11.1.3':
optional: true
'@pnpm/win-arm64@11.1.3':
optional: true
'@pnpm/win-x64@11.1.3':
optional: true
'@reflink/reflink-darwin-arm64@0.1.19':
optional: true
'@reflink/reflink-darwin-x64@0.1.19':
optional: true
'@reflink/reflink-linux-arm64-gnu@0.1.19':
optional: true
'@reflink/reflink-linux-arm64-musl@0.1.19':
optional: true
'@reflink/reflink-linux-x64-gnu@0.1.19':
optional: true
'@reflink/reflink-linux-x64-musl@0.1.19':
optional: true
'@reflink/reflink-win32-arm64-msvc@0.1.19':
optional: true
'@reflink/reflink-win32-x64-msvc@0.1.19':
optional: true
'@reflink/reflink@0.1.19':
optionalDependencies:
'@reflink/reflink-darwin-arm64': 0.1.19
'@reflink/reflink-darwin-x64': 0.1.19
'@reflink/reflink-linux-arm64-gnu': 0.1.19
'@reflink/reflink-linux-arm64-musl': 0.1.19
'@reflink/reflink-linux-x64-gnu': 0.1.19
'@reflink/reflink-linux-x64-musl': 0.1.19
'@reflink/reflink-win32-arm64-msvc': 0.1.19
'@reflink/reflink-win32-x64-msvc': 0.1.19
detect-libc@2.1.2: {}
pnpm@11.1.3: {}
---
lockfileVersion: '9.0'
settings:
@@ -8,7 +207,6 @@ overrides:
'@react-native/babel-preset': 0.81.5
'@react-native/normalize-colors': 0.81.5
'@expo/image-utils': 0.8.12
multiformats: 9.9.0
'@types/estree': 1.0.6
react-native-compressor: 1.13.0
react-native-reanimated: 3.19.1
@@ -44,11 +242,11 @@ importers:
.:
dependencies:
'@atproto/api':
specifier: ^0.19.19
version: 0.19.19
specifier: 0.20.5
version: 0.20.5
'@atproto/syntax':
specifier: 0.5.2
version: 0.5.2
specifier: 0.6.1
version: 0.6.1
'@bitdrift/react-native':
specifier: ^0.6.8
version: 0.6.14(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -56,13 +254,13 @@ importers:
specifier: ^6.0.2
version: 6.0.4
'@bsky.app/alf':
specifier: ^0.1.9
version: 0.1.9(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
specifier: ^0.1.14
version: 0.1.14(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@bsky.app/expo-guess-language':
specifier: ^0.2.8
version: 0.2.8(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@bsky.app/expo-image-crop-tool':
specifier: ^0.5.0
specifier: ^0.5.1
version: 0.5.1(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@bsky.app/expo-scroll-edge-effect':
specifier: ^0.1.4
@@ -74,14 +272,14 @@ importers:
specifier: 2.12.5
version: 2.12.5(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@bsky.app/sift':
specifier: ^0.3.4
version: 0.3.4(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
specifier: ^0.3.8
version: 0.3.8(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@bsky.app/tapper':
specifier: ^0.5.3
version: 0.5.3(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
specifier: ^0.5.7
version: 0.5.7(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@bsky.app/video':
specifier: 0.3.4
version: 0.3.4(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
specifier: 0.3.6
version: 0.3.6(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@discord/bottom-sheet':
specifier: bluesky-social/react-native-bottom-sheet
version: https://codeload.github.com/bluesky-social/react-native-bottom-sheet/tar.gz/28a87d1bb55e10fc355fa1455545a30734995908(patch_hash=253965341bfbc8e63630dfca511c5d0e0878900e5fe414b2278bd322fc215e2a)(@shopify/flash-list@2.3.1(@babel/runtime@7.29.2)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(@types/react@19.1.17)(react-native-gesture-handler@2.28.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -115,18 +313,6 @@ importers:
'@formatjs/intl-pluralrules':
specifier: ^5.4.6
version: 5.4.6
'@fortawesome/fontawesome-svg-core':
specifier: ^6.1.1
version: 6.7.2
'@fortawesome/free-regular-svg-icons':
specifier: ^6.1.1
version: 6.7.2
'@fortawesome/free-solid-svg-icons':
specifier: ^6.1.1
version: 6.7.2
'@fortawesome/react-native-fontawesome':
specifier: ^0.3.2
version: 0.3.2(@fortawesome/fontawesome-svg-core@6.7.2)(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))
'@growthbook/growthbook':
specifier: ^1.6.5
version: 1.6.5
@@ -134,7 +320,7 @@ importers:
specifier: ^1.6.5
version: 1.6.5(react@19.1.0)
'@ipld/dag-cbor':
specifier: ^9.2.0
specifier: ^9.2.7
version: 9.2.7
'@lingui/core':
specifier: ^5.9.2
@@ -389,8 +575,8 @@ importers:
specifier: ^4.1.1
version: 4.1.1
multiformats:
specifier: 9.9.0
version: 9.9.0
specifier: ^13.4.2
version: 13.4.2
nanoid:
specifier: ^5.0.5
version: 5.1.11
@@ -440,7 +626,7 @@ importers:
specifier: ^4.2.3
version: 4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(react-native-gesture-handler@2.28.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-edge-to-edge:
specifier: ^1.6.0
specifier: ^1.8.1
version: 1.8.1(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-gesture-handler:
specifier: ~2.28.0
@@ -636,8 +822,8 @@ importers:
specifier: ^17.0.0
version: 17.6.0
husky:
specifier: ^8.0.3
version: 8.0.3
specifier: ^9.1.7
version: 9.1.7
is-ci:
specifier: ^3.0.1
version: 3.0.1
@@ -653,6 +839,9 @@ importers:
lint-staged:
specifier: ^13.2.3
version: 13.3.0
node:
specifier: runtime:^24.15.0
version: runtime:24.15.0
prettier:
specifier: ^3.8.3
version: 3.8.3
@@ -688,29 +877,33 @@ packages:
graphql:
optional: true
'@atproto/api@0.19.19':
resolution: {integrity: sha512-8pwyN3qM1HnaJzL297OBjfxwLLJvNHkFDr5smoFRLXEDiyPprQfbsH7YrGsJfX8Cez+KcHZAuZTY4Hh6NUBwBQ==}
'@atproto/api@0.20.5':
resolution: {integrity: sha512-VqkRYKR9vRRk36NhtytJz5TPhNtR1hczCcfM+bWoOK6MBvWUT8HwelufrycFAIbFIH7n4ws+5UbnLYpTIBQZ6Q==}
engines: {node: '>=22'}
'@atproto/common-web@0.4.21':
resolution: {integrity: sha512-Odq+wdk3YNasGCjjlpl3bCIPvqYHige5DLfMkIffNv/2PI/iIj5ZvAvMvJlJ59OhReKSxtpI0invx5UQPc3+fw==}
'@atproto/common-web@0.5.0':
resolution: {integrity: sha512-ReWnkuZdDU/74/I47gaI26uxQjHmpq4edp41NnZZQ5vIIKGb7Ei6pZHzDTUD9JURo109SKrPx9RMP2IQm0fOKA==}
engines: {node: '>=22'}
'@atproto/lex-data@0.0.15':
resolution: {integrity: sha512-ZsbGiaM5S3CnGrcTMbDGON3bLZzCi/Mx9UvcMREKSRujnF68eHgMiXxJqvykP7+QpOX6tYCK93axZkuJVhtSEw==}
'@atproto/lex-data@0.1.1':
resolution: {integrity: sha512-/xza8nU/YhtzhETnHL3QKKofaJ28/0NCzhT7LaYoUkm8EgypWp5ykEtmW52yLhQM2JF6fVa25g1soQmNTGqtSg==}
engines: {node: '>=22'}
'@atproto/lex-json@0.0.16':
resolution: {integrity: sha512-IgLgQ0krshVlrIYZ+heTBDbCnM3LmAgWvsaYn5MxvKA3LcBot3PG3ptdO8VOweVZ+WgCLuo39cz9EbUmIbqdtg==}
'@atproto/lex-json@0.1.0':
resolution: {integrity: sha512-oWUrRMwFyWpmi/5k1Se3xBTbP06XdxBS5iFuUz9LmqItaPXwrWRD87a9ldPvINQ/A2/mn7J6/qug8sDVlhD+vQ==}
engines: {node: '>=22'}
'@atproto/lexicon@0.6.2':
resolution: {integrity: sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==}
'@atproto/lexicon@0.7.1':
resolution: {integrity: sha512-voNfNED5KUxn3vpo7N5DMRblBDfWf7kSfdKhJFC1RrLCxg38YbBzzURNVQJ32bp13Oot8kYfyXBWxTgtKLvw8w==}
engines: {node: '>=22'}
'@atproto/syntax@0.5.2':
resolution: {integrity: sha512-W41szOnkppoHr0iCUrzL8gy3OD6qmDyp1UvUgmTx2oFQfgbudpz51T/gznesiCcqiUT5obfHdx4PJ+WdlEOE7Q==}
'@atproto/syntax@0.6.1':
resolution: {integrity: sha512-kA4dQDoMPpWCH8N0Q4KoSq024u5MkVfDVa8DdhyLjGA72z/khbOf1jXKPv7NIL2oEc9aj7geKELdvqyf4ogopA==}
engines: {node: '>=22'}
'@atproto/syntax@0.5.4':
resolution: {integrity: sha512-9XJOpMAgsGFxMEIp8nJ8AIWv+krrY1xQMj+wULbbXhQztQV+9aZ0TbG9Jtn3Op2or8Kr6OqyWR4ga9Z189kKDw==}
'@atproto/xrpc@0.7.7':
resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==}
'@atproto/xrpc@0.8.0':
resolution: {integrity: sha512-NJy02bIKrWlE2NQkRV1kT0Cj0ixbuxlF/MejBdo4cPWAa9v3oZexvAcjjb0zaOYeABkaU14iyIhvn2G4e/oLpw==}
engines: {node: '>=22'}
'@babel/code-frame@7.10.4':
resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==}
@@ -1425,8 +1618,8 @@ packages:
'@braintree/sanitize-url@6.0.4':
resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==}
'@bsky.app/alf@0.1.9':
resolution: {integrity: sha512-dH8flBNXrEju62NSD0UNshrpzndzjbQRa/35MT8+nlWByQ9tChbW+62+xl+Oaut2YpgMsMmOuQ/CSZAS8Dwwtg==}
'@bsky.app/alf@0.1.14':
resolution: {integrity: sha512-c/KK6avyjEnYzhXDsN0rQaTFSbi8BPg4sJCW8aUk8AtKsO34GTaY6FU1adQ2i8xVfe5UIRu9FJjnBoGpuXj21A==}
peerDependencies:
react: '*'
react-native: '*'
@@ -1465,23 +1658,23 @@ packages:
react: '*'
react-native: '>=0.71.0'
'@bsky.app/sift@0.3.4':
resolution: {integrity: sha512-lOMltRODQVgzF9ssRH3eMG9fylvIrHwr4UFkhJMSW1ANB//Yxb30xgvwuGFd+q8H6Qp8/NF0HKTF6r6kRk/OeQ==}
'@bsky.app/sift@0.3.8':
resolution: {integrity: sha512-41Ug2QfjWJUApHsxcm6JDTb4r8jS6Qn6uBca8ofL10ZDKzXbSxDr1vK2yhFLyIeo/yIq3GU+XgEqrd1p6YgFiA==}
peerDependencies:
expo: '*'
react: '*'
react-native: '*'
react-native-safe-area-context: ~5.6.0
'@bsky.app/tapper@0.5.3':
resolution: {integrity: sha512-pf2GlTPlHYHcsA8E5BSxc9H7HWh/N2NIiV85ccN/gl1fZ02k0Nod44wVK5W9yVWkdunSn7mz0+9Y5of1TDaOaA==}
'@bsky.app/tapper@0.5.7':
resolution: {integrity: sha512-DWU4v+llcEdgwueittiCSMeahrwvQAz4g1S7M1gSSw3Zor987fF7WO/NTDWq7X/zFab2nOQ/lnCvQUuOgFCTRg==}
peerDependencies:
expo: '*'
react: '*'
react-native: '*'
'@bsky.app/video@0.3.4':
resolution: {integrity: sha512-BTHfdS5hWlpBvMSNhYhsAoBnWtu4BCXYyK/DZ1wA/akXquJP3SuzEGL6B2V0Xdtg2E0rhjJD2njlTj69E64reg==}
'@bsky.app/video@0.3.6':
resolution: {integrity: sha512-Ny7vz9v1lQOw5Llw4xejtJo/Dp/lmdRw1XdUJucOXlq6puXO93tv3mNXUIIyOEVWEwDPg8j/kSQxG/mGy+/hdQ==}
peerDependencies:
expo: '*'
react: '*'
@@ -1900,29 +2093,6 @@ packages:
'@formatjs/intl-pluralrules@5.4.6':
resolution: {integrity: sha512-2HlOq+c7KsSps829SJ3B5987coX5mzKx9NbPcNwQ07eq8FBHgB3HfMoxt5HvLsdk4oQwCjAEnocbtd+wVwZ2Kg==}
'@fortawesome/fontawesome-common-types@6.7.2':
resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==}
engines: {node: '>=6'}
'@fortawesome/fontawesome-svg-core@6.7.2':
resolution: {integrity: sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==}
engines: {node: '>=6'}
'@fortawesome/free-regular-svg-icons@6.7.2':
resolution: {integrity: sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==}
engines: {node: '>=6'}
'@fortawesome/free-solid-svg-icons@6.7.2':
resolution: {integrity: sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==}
engines: {node: '>=6'}
'@fortawesome/react-native-fontawesome@0.3.2':
resolution: {integrity: sha512-CiWfJWSZHRg12VXlaeFnaa5yJVPOrjsSFEvF6ntz3cnjg4oN3cvauL+JATacMCl0v9xzib32qC1WZAvvGkfB4w==}
peerDependencies:
'@fortawesome/fontawesome-svg-core': ~1 || ~6
react-native: '>= 0.67'
react-native-svg: '>= 11.x'
'@gorhom/portal@1.0.14':
resolution: {integrity: sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==}
peerDependencies:
@@ -4000,8 +4170,8 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
await-lock@2.2.2:
resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==}
await-lock@3.0.0:
resolution: {integrity: sha512-eO6fLiSnrJrMdjWMNK8zbVRXPs2TKJg78iKZd9wDpN3na5tcoV6EoeiOlMgk2QaAQ1gIrK1YuMsJHXWqz89tSA==}
babel-jest@29.7.0:
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
@@ -5882,12 +6052,9 @@ packages:
resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==}
engines: {node: '>=14.18.0'}
humps@2.0.1:
resolution: {integrity: sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==}
husky@8.0.3:
resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==}
engines: {node: '>=14'}
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
hasBin: true
hyphenate-style-name@1.1.0:
@@ -6907,8 +7074,8 @@ packages:
resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==}
hasBin: true
multiformats@9.9.0:
resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==}
multiformats@13.4.2:
resolution: {integrity: sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@@ -6974,6 +7141,127 @@ packages:
node-releases@2.0.44:
resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==}
node@runtime:24.15.0:
resolution:
type: variations
variants:
- resolution:
archive: tarball
bin:
node: bin/node
integrity: sha256-3UvHfctfTJoslkNzm7WTUAzLCUE+xCyqD47w5e8RYJU=
type: binary
url: https://nodejs.org/download/release/v24.15.0/node-v24.15.0-aix-ppc64.tar.gz
targets:
- cpu: ppc64
os: aix
- resolution:
archive: tarball
bin:
node: bin/node
integrity: sha256-NyMxuWl3mrXRW5SYhPxur4jVr+h73ouogdZAC5EA/8Q=
type: binary
url: https://nodejs.org/download/release/v24.15.0/node-v24.15.0-darwin-arm64.tar.gz
targets:
- cpu: arm64
os: darwin
- resolution:
archive: tarball
bin:
node: bin/node
integrity: sha256-/9XuKTRnkn8+5zGlU+uI/R9Iz3TuvC10prq+SvIoZzs=
type: binary
url: https://nodejs.org/download/release/v24.15.0/node-v24.15.0-darwin-x64.tar.gz
targets:
- cpu: x64
os: darwin
- resolution:
archive: tarball
bin:
node: bin/node
integrity: sha256-c6/CNNVYwkkZh19RwtHqACoq2k6m+DYBo4OGn++mTu0=
type: binary
url: https://nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-arm64.tar.gz
targets:
- cpu: arm64
os: linux
- resolution:
archive: tarball
bin:
node: bin/node
integrity: sha256-sfiJAKSxY2XqulYmkwT8Edp1gdvwNVLUSV+azh/AX20=
type: binary
url: https://nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-ppc64le.tar.gz
targets:
- cpu: ppc64le
os: linux
- resolution:
archive: tarball
bin:
node: bin/node
integrity: sha256-+YVFVDnVL+m43mqPbQe/7MxzbepSfofqyv5ajXUWo4A=
type: binary
url: https://nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-s390x.tar.gz
targets:
- cpu: s390x
os: linux
- resolution:
archive: tarball
bin:
node: bin/node
integrity: sha256-RINoctmuxJ8ea1KpqSKHLbmisC0jWmFqVoG2qF/sjYk=
type: binary
url: https://nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-x64.tar.gz
targets:
- cpu: x64
os: linux
- resolution:
archive: zip
bin:
node: node.exe
integrity: sha256-yet0Au2ibiun5EtnJ/yFqN5WxQlbH3Hr0wYokiEaoRY=
prefix: node-v24.15.0-win-arm64
type: binary
url: https://nodejs.org/download/release/v24.15.0/node-v24.15.0-win-arm64.zip
targets:
- cpu: arm64
os: win32
- resolution:
archive: zip
bin:
node: node.exe
integrity: sha256-zFFJ6r1Td5zh573FQBZDYi0MfmgAreGJKKdn6UC7DmI=
prefix: node-v24.15.0-win-x64
type: binary
url: https://nodejs.org/download/release/v24.15.0/node-v24.15.0-win-x64.zip
targets:
- cpu: x64
os: win32
- resolution:
archive: tarball
bin:
node: bin/node
integrity: sha256-MemKqWCgZ9qR7f/V2TvEZle10qgClhLDWfXyrABgFSo=
type: binary
url: https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-arm64-musl.tar.gz
targets:
- cpu: arm64
os: linux
libc: musl
- resolution:
archive: tarball
bin:
node: bin/node
integrity: sha256-9Vr1vUicU0exE8pllMrgClSzC6V6xYdTJDEb/G9HYuM=
type: binary
url: https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-x64-musl.tar.gz
targets:
- cpu: x64
os: linux
libc: musl
version: 24.15.0
hasBin: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@@ -8733,8 +9021,8 @@ packages:
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
uint8arrays@3.0.0:
resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==}
uint8arrays@5.1.1:
resolution: {integrity: sha512-9muQwa4wZG4dKi9gMAIBtnk2Pw87SRpvWTH6lOGm19V2Uqxr4uomUf2PGqPnWc+qs06sN8owUU4jfcoWOcfwVQ==}
unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
@@ -9091,8 +9379,8 @@ packages:
utf-8-validate:
optional: true
ws@8.20.0:
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
ws@8.20.1:
resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -9206,55 +9494,51 @@ snapshots:
'@0no-co/graphql.web@1.2.0': {}
'@atproto/api@0.19.19':
'@atproto/api@0.20.5':
dependencies:
'@atproto/common-web': 0.4.21
'@atproto/lexicon': 0.6.2
'@atproto/syntax': 0.5.4
'@atproto/xrpc': 0.7.7
await-lock: 2.2.2
multiformats: 9.9.0
'@atproto/common-web': 0.5.0
'@atproto/lexicon': 0.7.1
'@atproto/syntax': 0.6.1
'@atproto/xrpc': 0.8.0
await-lock: 3.0.0
multiformats: 13.4.2
tlds: 1.261.0
zod: 3.25.76
'@atproto/common-web@0.4.21':
'@atproto/common-web@0.5.0':
dependencies:
'@atproto/lex-data': 0.0.15
'@atproto/lex-json': 0.0.16
'@atproto/syntax': 0.5.4
'@atproto/lex-data': 0.1.1
'@atproto/lex-json': 0.1.0
'@atproto/syntax': 0.6.1
zod: 3.25.76
'@atproto/lex-data@0.0.15':
'@atproto/lex-data@0.1.1':
dependencies:
multiformats: 9.9.0
multiformats: 13.4.2
tslib: 2.8.1
uint8arrays: 3.0.0
uint8arrays: 5.1.1
unicode-segmenter: 0.14.5
'@atproto/lex-json@0.0.16':
'@atproto/lex-json@0.1.0':
dependencies:
'@atproto/lex-data': 0.0.15
'@atproto/lex-data': 0.1.1
tslib: 2.8.1
'@atproto/lexicon@0.6.2':
'@atproto/lexicon@0.7.1':
dependencies:
'@atproto/common-web': 0.4.21
'@atproto/syntax': 0.5.2
iso-datestring-validator: 2.2.2
multiformats: 9.9.0
'@atproto/common-web': 0.5.0
'@atproto/syntax': 0.6.1
multiformats: 13.4.2
zod: 3.25.76
'@atproto/syntax@0.5.2':
'@atproto/syntax@0.6.1':
dependencies:
iso-datestring-validator: 2.2.2
tslib: 2.8.1
'@atproto/syntax@0.5.4':
'@atproto/xrpc@0.8.0':
dependencies:
tslib: 2.8.1
'@atproto/xrpc@0.7.7':
dependencies:
'@atproto/lexicon': 0.6.2
'@atproto/lexicon': 0.7.1
zod: 3.25.76
'@babel/code-frame@7.10.4':
@@ -10157,7 +10441,7 @@ snapshots:
'@braintree/sanitize-url@6.0.4': {}
'@bsky.app/alf@0.1.9(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
'@bsky.app/alf@0.1.14(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
@@ -10193,20 +10477,20 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
'@bsky.app/sift@0.3.4(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
'@bsky.app/sift@0.3.8(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
expo: 54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@bsky.app/tapper@0.5.3(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
'@bsky.app/tapper@0.5.7(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
expo: 54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
'@bsky.app/video@0.3.4(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
'@bsky.app/video@0.3.6(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
expo: 54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react: 19.1.0
@@ -10452,7 +10736,7 @@ snapshots:
terminal-link: 2.1.1
undici: 6.25.0
wrap-ansi: 7.0.0
ws: 8.20.0
ws: 8.20.1
optionalDependencies:
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
transitivePeerDependencies:
@@ -10822,28 +11106,6 @@ snapshots:
decimal.js: 10.6.0
tslib: 2.8.1
'@fortawesome/fontawesome-common-types@6.7.2': {}
'@fortawesome/fontawesome-svg-core@6.7.2':
dependencies:
'@fortawesome/fontawesome-common-types': 6.7.2
'@fortawesome/free-regular-svg-icons@6.7.2':
dependencies:
'@fortawesome/fontawesome-common-types': 6.7.2
'@fortawesome/free-solid-svg-icons@6.7.2':
dependencies:
'@fortawesome/fontawesome-common-types': 6.7.2
'@fortawesome/react-native-fontawesome@0.3.2(@fortawesome/fontawesome-svg-core@6.7.2)(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))':
dependencies:
'@fortawesome/fontawesome-svg-core': 6.7.2
humps: 2.0.1
prop-types: 15.8.1
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
react-native-svg: 15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@gorhom/portal@1.0.14(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
nanoid: 3.3.12
@@ -10880,7 +11142,7 @@ snapshots:
'@ipld/dag-cbor@9.2.7':
dependencies:
cborg: 5.1.1
multiformats: 9.9.0
multiformats: 13.4.2
'@isaacs/cliui@8.0.2':
dependencies:
@@ -13225,7 +13487,7 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
await-lock@2.2.2: {}
await-lock@3.0.0: {}
babel-jest@29.7.0(@babel/core@7.29.0):
dependencies:
@@ -13836,7 +14098,7 @@ snapshots:
postcss-modules-scope: 3.2.1(postcss@8.5.14)
postcss-modules-values: 4.0.0(postcss@8.5.14)
postcss-value-parser: 4.2.0
semver: 7.5.4
semver: 7.8.0
optionalDependencies:
webpack: 5.106.2(postcss@8.5.14)
@@ -14128,7 +14390,7 @@ snapshots:
dotenv-expand@11.0.7:
dependencies:
dotenv: 16.4.7
dotenv: 16.6.1
dotenv@16.4.7: {}
@@ -15447,9 +15709,7 @@ snapshots:
human-signals@4.3.1: {}
humps@2.0.1: {}
husky@8.0.3: {}
husky@9.1.7: {}
hyphenate-style-name@1.1.0: {}
@@ -16208,7 +16468,7 @@ snapshots:
whatwg-encoding: 2.0.0
whatwg-mimetype: 3.0.0
whatwg-url: 11.0.0
ws: 8.20.0
ws: 8.20.1
xml-name-validator: 4.0.0
transitivePeerDependencies:
- bufferutil
@@ -16754,7 +17014,7 @@ snapshots:
dns-packet: 5.6.1
thunky: 1.1.0
multiformats@9.9.0: {}
multiformats@13.4.2: {}
mz@2.7.0:
dependencies:
@@ -16805,6 +17065,8 @@ snapshots:
node-releases@2.0.44: {}
node@runtime:24.15.0: {}
normalize-path@3.0.0: {}
normalize-url@6.1.0: {}
@@ -18746,9 +19008,9 @@ snapshots:
uc.micro@2.1.0: {}
uint8arrays@3.0.0:
uint8arrays@5.1.1:
dependencies:
multiformats: 9.9.0
multiformats: 13.4.2
unbox-primitive@1.1.0:
dependencies:
@@ -18998,7 +19260,7 @@ snapshots:
sockjs: 0.3.24
spdy: 4.0.2
webpack-dev-middleware: 5.3.4(webpack@5.106.2(postcss@8.5.14))
ws: 8.20.0
ws: 8.20.1
optionalDependencies:
webpack: 5.106.2(postcss@8.5.14)
transitivePeerDependencies:
@@ -19176,7 +19438,7 @@ snapshots:
ws@7.5.10: {}
ws@8.20.0: {}
ws@8.20.1: {}
xcode@3.0.1:
dependencies:
+2 -2
View File
@@ -1,11 +1,12 @@
nodeLinker: 'hoisted'
autoInstallPeers: true # default: true
strictPeerDependencies: false # default: false
trustPolicy: 'no-downgrade' # default: off
trustPolicyIgnoreAfter: 10080 # 7 days, default: undefined
overrides:
'@react-native/babel-preset': '0.81.5'
'@react-native/normalize-colors': '0.81.5'
'@expo/image-utils': '0.8.12'
'multiformats': '9.9.0'
'@types/estree': '1.0.6'
'react-native-compressor': '1.13.0'
'react-native-reanimated': '3.19.1'
@@ -15,7 +16,6 @@ overrides:
allowBuilds:
'@sentry/cli': true
'core-js-pure': true
'dtrace-provider': true
'esbuild': true
'unrs-resolver': true
patchedDependencies:
+5 -1
View File
@@ -1,4 +1,6 @@
module.exports = {
import {type Config} from 'prettier'
const config: Config = {
semi: false,
arrowParens: 'avoid',
bracketSameLine: true,
@@ -6,3 +8,5 @@ module.exports = {
singleQuote: true,
trailingComma: 'all',
}
export default config
+15 -7
View File
@@ -136,7 +136,14 @@ for (const [pkg, files] of groups) {
try {
execFileSync(
'patch',
[`-p${stripLevel}`, '-l', '-N', '--no-backup-if-mismatch', '-i', patchPath],
[
`-p${stripLevel}`,
'-l',
'-N',
'--no-backup-if-mismatch',
'-i',
patchPath,
],
{cwd: tmp, stdio: 'pipe'},
)
console.log(` applied ${file} (via GNU patch -l)`)
@@ -146,16 +153,17 @@ for (const [pkg, files] of groups) {
// If reversing applies, the patch is already in upstream -- stale.
try {
execFileSync(
'git',
[...gitArgs, '--reverse', '--check', patchPath],
{cwd: tmp, stdio: 'pipe'},
)
execFileSync('git', [...gitArgs, '--reverse', '--check', patchPath], {
cwd: tmp,
stdio: 'pipe',
})
console.log(` STALE ${file} (already in upstream, skipping)`)
stale++
} catch {
console.error(` FAILED ${file} (does not apply, not stale)`)
throw new Error(`patch ${file} does not apply against ${pkg}@${version}`)
throw new Error(
`patch ${file} does not apply against ${pkg}@${version}`,
)
}
}
-1
View File
@@ -1,5 +1,4 @@
import '#/logger/sentry/setup'
import '#/view/icons'
import {Fragment, useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
-1
View File
@@ -1,5 +1,4 @@
import '#/logger/sentry/setup' // must be near top
import '#/view/icons'
import './style.css'
import {Fragment, useEffect, useState} from 'react'
+20 -8
View File
@@ -22,8 +22,9 @@ import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {
type ChatNotificationPayload,
getNotificationPayload,
type NotificationPayload,
isChatNotificationPayload,
notificationToURL,
storePayloadForAccountSwitch,
} from '#/lib/hooks/useNotificationHandler'
@@ -926,14 +927,14 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
const linkingUrl = Linking.useLinkingURL()
/**
* Handle navigation to a conversation, or prepares for account switch.
* Handle navigation to the messages tab, or prepares for account switch.
*
* Non-reactive because we need the latest data from some hooks
* after an async call - sfn
*/
const handleChatMessage = useNonReactiveCallback(
(payload: Extract<NotificationPayload, {reason: 'chat-message'}>) => {
notyLogger.debug(`handleChatMessage`, {payload})
const handleChatNotification = useNonReactiveCallback(
(payload: ChatNotificationPayload) => {
notyLogger.debug(`handleChatNotification`, {payload})
if (payload.recipientDid !== currentAccount?.did) {
// handled in useNotificationHandler after account switch finishes
@@ -946,7 +947,13 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
} else {
setShowLoggedOut(true)
}
} else {
} else if (
payload.reason === 'chat-message' ||
payload.reason === 'chat-reaction' ||
payload.reason === 'chat-added-to-group'
) {
// chat-added-to-group routes to the convo because the recipient was
// just added and now has access.
// @ts-expect-error nested navigators aren't typed -sfn
navigate('MessagesTab', {
screen: 'Messages',
@@ -954,6 +961,11 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
pushToConversation: payload.convoId,
},
})
} else {
// chat-removed-from-group, chat-join-request-rejected: the convo is
// no longer accessible to the recipient, so just open the list.
// @ts-expect-error nested navigators aren't typed -sfn
navigate('MessagesTab', {screen: 'Messages'})
}
},
)
@@ -989,8 +1001,8 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
causedBoot: true,
})
if (payload.reason === 'chat-message') {
handleChatMessage(payload)
if (isChatNotificationPayload(payload)) {
handleChatNotification(payload)
} else {
const path = notificationToURL(payload)
+32 -22
View File
@@ -1,5 +1,5 @@
import {createContext, useCallback, useContext, useMemo, useState} from 'react'
import {type Theme, type ThemeName} from '@bsky.app/alf'
import {type Theme, type ThemeName, utils as baseUtils} from '@bsky.app/alf'
import {
computeFontScaleMultiplier,
@@ -9,14 +9,10 @@ import {
setFontScale as persistFontScale,
} from '#/alf/fonts'
import {themes} from '#/alf/themes'
import {darken, lighten, rgbToHex} from '#/alf/util/colorGeneration'
import {type Device} from '#/storage'
export {
type TextStyleProp,
type Theme,
utils,
type ViewStyleProp,
} from '@bsky.app/alf'
export {type TextStyleProp, type Theme, type ViewStyleProp} from '@bsky.app/alf'
export {atoms} from '#/alf/atoms'
export * from '#/alf/breakpoints'
export * from '#/alf/fonts'
@@ -25,6 +21,12 @@ export * from '#/alf/util/flatten'
export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector'
export * from '#/alf/util/useGutters'
export const utils = {
...baseUtils,
rgbToHex,
lighten,
darken,
}
export type Alf = {
themeName: ThemeName
@@ -64,7 +66,11 @@ Context.displayName = 'AlfContext'
export function ThemeProvider({
children,
theme: themeName,
}: React.PropsWithChildren<{theme: ThemeName}>) {
themesOverride,
}: React.PropsWithChildren<{
theme: ThemeName
themesOverride?: Partial<typeof themes>
}>) {
const [fontScale, setFontScale] = useState<Alf['fonts']['scale']>(() =>
getFontScale(),
)
@@ -90,11 +96,15 @@ export function ThemeProvider({
[setFontFamily],
)
const value = useMemo<Alf>(
() => ({
themes,
const value = useMemo<Alf>(() => {
const t = {
...themes,
...themesOverride,
}
return {
themes: t,
themeName: themeName,
theme: themes[themeName],
theme: t[themeName],
fonts: {
scale: fontScale,
scaleMultiplier: fontScaleMultiplier,
@@ -103,16 +113,16 @@ export function ThemeProvider({
setFontFamily: setFontFamilyAndPersist,
},
flags: {},
}),
[
themeName,
fontScale,
setFontScaleAndPersist,
fontFamily,
setFontFamilyAndPersist,
fontScaleMultiplier,
],
)
}
}, [
themeName,
fontScale,
setFontScaleAndPersist,
fontFamily,
setFontFamilyAndPersist,
fontScaleMultiplier,
themesOverride,
])
return <Context.Provider value={value}>{children}</Context.Provider>
}
+94
View File
@@ -0,0 +1,94 @@
import {darken, hexToRgb, lighten, rgbToHex} from './colorGeneration'
describe('hexToRgb', () => {
it('parses 6-digit hex', () => {
expect(hexToRgb('#abcdef')).toEqual({r: 0xab, g: 0xcd, b: 0xef})
})
it('parses 6-digit hex without leading #', () => {
expect(hexToRgb('abcdef')).toEqual({r: 0xab, g: 0xcd, b: 0xef})
})
it('parses 3-digit shorthand hex', () => {
expect(hexToRgb('#abc')).toEqual({r: 0xaa, g: 0xbb, b: 0xcc})
})
it('parses 8-digit hex by dropping the alpha channel', () => {
expect(hexToRgb('#aabbccdd')).toEqual({r: 0xaa, g: 0xbb, b: 0xcc})
})
it('handles uppercase digits', () => {
expect(hexToRgb('#ABCDEF')).toEqual({r: 0xab, g: 0xcd, b: 0xef})
})
it('returns null for 4-digit shorthand', () => {
expect(hexToRgb('#abcd')).toBeNull()
})
it('returns null for non-hex characters', () => {
expect(hexToRgb('#zzzzzz')).toBeNull()
})
it('returns null for non-hex words', () => {
expect(hexToRgb('blue')).toBeNull()
})
it('returns null for the empty string', () => {
expect(hexToRgb('')).toBeNull()
})
it('returns null for unexpected lengths', () => {
expect(hexToRgb('#ab')).toBeNull()
expect(hexToRgb('#abcde')).toBeNull()
expect(hexToRgb('#abcdefg')).toBeNull()
})
})
describe('rgbToHex', () => {
it('formats integer channels', () => {
expect(rgbToHex(0xab, 0xcd, 0xef)).toBe('#abcdef')
})
it('rounds floating-point channels', () => {
expect(rgbToHex(170.4, 187.6, 204.5)).toBe('#aabccd')
})
it('clamps below zero to 00', () => {
expect(rgbToHex(-10, 0, 0)).toBe('#000000')
})
it('clamps above 255 to ff', () => {
expect(rgbToHex(300, 255, 255)).toBe('#ffffff')
})
it('zero-pads short hex output', () => {
expect(rgbToHex(0, 0, 0)).toBe('#000000')
})
})
describe('lighten / darken', () => {
it('lighten increases lightness', () => {
expect(lighten('#808080', 10)).toBe('#9a9a9a')
})
it('darken decreases lightness', () => {
expect(darken('#808080', 10)).toBe('#676767')
})
it('lighten clamps at white', () => {
expect(lighten('#ffffff', 50)).toBe('#ffffff')
})
it('darken clamps at black', () => {
expect(darken('#000000', 50)).toBe('#000000')
})
it('lighten by zero is a no-op', () => {
expect(lighten('#abcdef', 0)).toBe('#abcdef')
})
it('returns the input unchanged for invalid hex', () => {
expect(lighten('not-a-color', 10)).toBe('not-a-color')
expect(darken('#zzz', 10)).toBe('#zzz')
})
})
+130
View File
@@ -6,3 +6,133 @@ export const BLUE_HUE = 211
* @deprecated use `utils.alpha` from `@bsky.app/alf` instead
*/
export const transparentifyColor = utils.alpha
/**
* Lighten a hex color by `amount` percentage points of HSL lightness (0-100).
* If `hex` is not a valid hex color, returns the input unchanged.
*/
export function lighten(hex: string, amount: number): string {
return adjustLightness(hex, amount)
}
/**
* Darken a hex color by `amount` percentage points of HSL lightness (0-100).
* If `hex` is not a valid hex color, returns the input unchanged.
*/
export function darken(hex: string, amount: number): string {
return adjustLightness(hex, -amount)
}
function adjustLightness(hex: string, delta: number): string {
try {
const rgb = hexToRgb(hex)
if (!rgb) return hex
const {h, s, l} = rgbToHsl(rgb.r, rgb.g, rgb.b)
const next = clamp(l + delta, 0, 100)
const out = hslToRgb(h, s, next)
return rgbToHex(out.r, out.g, out.b)
} catch {
return hex
}
}
const HEX_PATTERN = /^([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/
export function hexToRgb(
hex: string,
): {r: number; g: number; b: number} | null {
try {
if (typeof hex !== 'string') return null
const h = hex.startsWith('#') ? hex.slice(1) : hex
if (!HEX_PATTERN.test(h)) return null
// 8-digit hex carries an alpha channel we don't use; drop it
const rgb = h.length === 8 ? h.slice(0, 6) : h
const expanded =
rgb.length === 3
? rgb
.split('')
.map(c => c + c)
.join('')
: rgb
const num = parseInt(expanded, 16)
return {
r: (num >> 16) & 255,
g: (num >> 8) & 255,
b: num & 255,
}
} catch {
return null
}
}
export function rgbToHex(r: number, g: number, b: number): string {
const c = (n: number) => clamp(Math.round(n), 0, 255)
return `#${((1 << 24) + (c(r) << 16) + (c(g) << 8) + c(b))
.toString(16)
.slice(1)}`
}
function rgbToHsl(
r: number,
g: number,
b: number,
): {h: number; s: number; l: number} {
r /= 255
g /= 255
b /= 255
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const l = (max + min) / 2
let h = 0
let s = 0
if (max !== min) {
const d = max - min
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0)
break
case g:
h = (b - r) / d + 2
break
case b:
h = (r - g) / d + 4
break
}
h /= 6
}
return {h: h * 360, s: s * 100, l: l * 100}
}
function hslToRgb(
h: number,
s: number,
l: number,
): {r: number; g: number; b: number} {
h /= 360
s /= 100
l /= 100
if (s === 0) {
const v = l * 255
return {r: v, g: v, b: v}
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
const p = 2 * l - q
const f = (t: number) => {
if (t < 0) t += 1
if (t > 1) t -= 1
if (t < 1 / 6) return p + (q - p) * 6 * t
if (t < 1 / 2) return q
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
return p
}
return {
r: f(h + 1 / 3) * 255,
g: f(h) * 255,
b: f(h - 1 / 3) * 255,
}
}
function clamp(n: number, min: number, max: number): number {
return Math.max(min, Math.min(max, n))
}
+8 -10
View File
@@ -24,6 +24,7 @@ import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {
BUTTON_VISUAL_ALIGNMENT_OFFSET,
CENTER_COLUMN_OFFSET,
CENTER_COLUMN_WIDTH,
HEADER_SLOT_SIZE,
SCROLLBAR_OFFSET,
} from '#/components/Layout/const'
@@ -48,7 +49,7 @@ export function Outer({
const {gtMobile} = useBreakpoints()
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
const {centerColumnOffset} = useLayoutBreakpoints()
const {isWithinSplitView} = useIsWithinSplitView()
const {isWithinSplitView, isWithinLeftPanel} = useIsWithinSplitView()
return (
<View
@@ -60,13 +61,13 @@ export function Outer({
a.align_center,
a.gap_sm,
sticky && web([a.sticky, {top: 0}, a.z_10, t.atoms.bg]),
gutters,
isWithinLeftPanel ? a.px_lg : gutters,
platform({
native: [a.pb_xs, {minHeight: 48}],
web: [a.py_xs, {minHeight: 52}],
}),
t.atoms.border_contrast_low,
gtMobile && [a.mx_auto, {maxWidth: 600}],
gtMobile && [a.mx_auto, {maxWidth: CENTER_COLUMN_WIDTH}],
!isWithinOffsetView &&
!isWithinSplitView && {
transform: [
@@ -112,7 +113,6 @@ export function Slot({children}: {children?: React.ReactNode}) {
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const {isWithinRightPanel} = useIsWithinSplitView()
const onPressBack = useCallback(
(evt: GestureResponderEvent) => {
@@ -127,10 +127,6 @@ export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
[onPress, navigation],
)
if (isWithinRightPanel) {
return null
}
return (
<Slot>
<Button
@@ -190,12 +186,14 @@ export function TitleText({
style,
}: {children: React.ReactNode} & TextStyleProp) {
const {gtMobile} = useBreakpoints()
const {isWithinLeftPanel} = useIsWithinSplitView()
const align = useContext(AlignmentContext)
return (
<Text
style={[
a.text_lg,
a.font_semi_bold,
isWithinLeftPanel
? [a.text_xl, a.font_bold]
: [a.text_lg, a.font_semi_bold],
a.leading_tight,
IS_IOS && align === 'platform' && a.text_center,
gtMobile && a.text_xl,
+7 -2
View File
@@ -1,7 +1,7 @@
export const SCROLLBAR_OFFSET =
'calc(-1 * var(--removed-body-scroll-bar-size, 0px) / 2)' as any
'calc(-1 * var(--removed-body-scroll-bar-size, 0px) / 2)' as `${number}%`
export const SCROLLBAR_OFFSET_POSITIVE =
'calc(var(--removed-body-scroll-bar-size, 0px) / 2)' as any
'calc(var(--removed-body-scroll-bar-size, 0px) / 2)' as `${number}%`
/**
* Useful for visually aligning icons within header buttons with the elements
@@ -19,3 +19,8 @@ export const HEADER_SLOT_SIZE = 33
* How far to shift the center column when in the tablet breakpoint
*/
export const CENTER_COLUMN_OFFSET = -105
/**
* How wide the center column is
*/
export const CENTER_COLUMN_WIDTH = 600
+6 -2
View File
@@ -22,7 +22,11 @@ import {
web,
} from '#/alf'
import {useDialogContext} from '#/components/Dialog'
import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const'
import {
CENTER_COLUMN_OFFSET,
CENTER_COLUMN_WIDTH,
SCROLLBAR_OFFSET,
} from '#/components/Layout/const'
import {ScrollbarOffsetContext} from '#/components/Layout/context'
import {IS_WEB} from '#/env'
@@ -152,7 +156,7 @@ export const Center = memo(function LayoutCenter({
a.w_full,
!isWithinSplitView && a.mx_auto,
gtMobile && {
maxWidth: 600,
maxWidth: CENTER_COLUMN_WIDTH,
},
!isWithinOffsetView &&
!isWithinSplitView && {
+2 -2
View File
@@ -19,8 +19,8 @@ export function Lightbox() {
<ImageView
lightbox={activeLightbox}
onRequestClose={onClose}
onPressSave={saveImageToAlbum}
onPressShare={uri => shareImageModal({uri})}
onPressSave={uri => void saveImageToAlbum(uri)}
onPressShare={uri => void shareImageModal({uri})}
/>
)
}
+23 -28
View File
@@ -1,9 +1,7 @@
import {useCallback, useEffect, useRef, useState} from 'react'
import {Pressable, StyleSheet, View} from 'react-native'
import {Image} from 'expo-image'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {FocusGuards, FocusScope} from 'radix-ui/internal'
import {RemoveScrollBar} from 'react-remove-scroll-bar'
@@ -67,12 +65,12 @@ function LightboxContainer({
children: React.ReactNode
handleBackgroundPress: () => void
}) {
const {_} = useLingui()
const {t: l} = useLingui()
FocusGuards.useFocusGuards()
return (
<Pressable
accessibilityHint={undefined}
accessibilityLabel={_(msg`Close image viewer`)}
accessibilityLabel={l`Close image viewer`}
onPress={handleBackgroundPress}
style={[a.fixed, a.inset_0, a.z_10]}>
<Backdrop />
@@ -81,7 +79,7 @@ function LightboxContainer({
<div
role="dialog"
aria-modal="true"
aria-label={_(msg`Image viewer`)}
aria-label={l`Image viewer`}
style={{position: 'absolute', inset: 0}}>
{children}
</div>
@@ -100,7 +98,7 @@ function LightboxGallery({
onClose: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const {reduceMotionEnabled} = useA11y()
const [index, setIndex] = useState(initialIndex)
const [hasAnyLoaded, setAnyHasLoaded] = useState(false)
@@ -196,7 +194,7 @@ function LightboxGallery({
]}
hoverStyle={styles.blurredBackdropHover}
color="secondary"
label={_(msg`Previous image`)}
label={l`Previous image`}
shape="round"
size={gtPhone ? 'large' : 'small'}>
<ChevronLeftIcon
@@ -217,7 +215,7 @@ function LightboxGallery({
]}
hoverStyle={styles.blurredBackdropHover}
color="secondary"
label={_(msg`Next image`)}
label={l`Next image`}
shape="round"
size={gtPhone ? 'large' : 'small'}>
<ChevronRightIcon
@@ -236,10 +234,8 @@ function LightboxGallery({
delayedFadeInAnim,
]}>
<Pressable
accessibilityLabel={_(msg`Expand alt text`)}
accessibilityHint={_(
msg`If alt text is long, toggles alt text expanded state`,
)}
accessibilityLabel={l`Expand alt text`}
accessibilityHint={l`If alt text is long, toggles alt text expanded state`}
onPress={() => {
setAltExpanded(!isAltExpanded)
}}>
@@ -254,28 +250,27 @@ function LightboxGallery({
) : null}
{imgs.length > 1 && (
<div aria-live="polite" aria-atomic="true" style={a.sr_only}>
<Text>{_(msg`Image ${index + 1} of ${imgs.length}`)}</Text>
<Text>{l`Image ${index + 1} of ${imgs.length}`}</Text>
</div>
)}
<Menu.Root>
<Menu.Trigger label={_(msg`Image options`)}>
<Menu.Trigger label={l`Image options`}>
{({props}) => (
<Pressable
{...props}
accessible={false}
style={[a.absolute, styles.menuBtn, delayedFadeInAnim]}>
<View style={[a.absolute, styles.menuBtn, delayedFadeInAnim]}>
<CircleChromeButton
{...props}
accessible={false}
icon={EllipsisIcon}
iconStyle={{transform: [{rotate: '90deg'}]}}
label={_(msg`Image options`)}
label={l`Image options`}
/>
</Pressable>
</View>
)}
</Menu.Trigger>
<Menu.Outer>
<Menu.Group>
<Menu.Item
label={_(msg`Share image`)}
label={l`Share image`}
onPress={async () => {
const url = img.uri
if (
@@ -294,9 +289,9 @@ function LightboxGallery({
) {
try {
await navigator.clipboard.writeText(url)
Toast.show(_(msg`Link copied to clipboard`))
Toast.show(l`Link copied to clipboard`)
} catch {
Toast.show(_(msg`Failed to copy link`), {type: 'error'})
Toast.show(l`Failed to copy link`, {type: 'error'})
}
}
}}>
@@ -306,14 +301,14 @@ function LightboxGallery({
<Menu.ItemIcon icon={ShareIcon} position="right" />
</Menu.Item>
<Menu.Item
label={_(msg`Download image`)}
label={l`Download image`}
onPress={() => {
saveImageToMediaLibrary({uri: img.uri}).then(
() => {
Toast.show(_(msg`Image saved`))
Toast.show(l`Image saved`)
},
() => {
Toast.show(_(msg`Failed to save image`), {type: 'error'})
Toast.show(l`Failed to save image`, {type: 'error'})
},
)
}}>
@@ -328,7 +323,7 @@ function LightboxGallery({
<View style={[a.absolute, styles.closeBtn, delayedFadeInAnim]}>
<CircleChromeButton
icon={XIcon}
label={_(msg`Close image viewer`)}
label={l`Close image viewer`}
onPress={onClose}
/>
</View>
@@ -8,7 +8,7 @@ import {
} from 'react-native'
import {BlurView} from 'expo-blur'
import {HITSLOP_10} from '#/lib/constants'
import {HITSLOP_20} from '#/lib/constants'
import {type Props as IconProps} from '#/components/icons/common'
type Props = {
@@ -17,11 +17,19 @@ type Props = {
label: string
onPress?: PressableProps['onPress']
testID?: string
}
} & Omit<
PressableProps,
| 'onPress'
| 'style'
| 'testID'
| 'accessibilityRole'
| 'accessibilityLabel'
| 'accessibilityHint'
>
const SIZE = 44
const RADIUS = 24
const ICON = 24
const SIZE = 32
const RADIUS = SIZE / 2
const ICON = 18
export function CircleChromeButton({
icon: Icon,
@@ -29,13 +37,15 @@ export function CircleChromeButton({
label,
onPress,
testID,
...rest
}: Props) {
return (
<Pressable
{...rest}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityHint=""
hitSlop={HITSLOP_10}
hitSlop={HITSLOP_20}
onPress={onPress}
testID={testID}
style={({pressed}) => [styles.root, pressed && styles.pressed]}>
@@ -17,7 +17,15 @@ type Props = {
label: string
onPress?: PressableProps['onPress']
testID?: string
}
} & Omit<
PressableProps,
| 'onPress'
| 'style'
| 'testID'
| 'accessibilityRole'
| 'accessibilityLabel'
| 'accessibilityHint'
>
const SIZE = 44
const RADIUS = 24
@@ -29,9 +37,11 @@ export function CircleChromeButton({
label,
onPress,
testID,
...rest
}: Props) {
return (
<Pressable
{...rest}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityHint=""
+14 -17
View File
@@ -7,10 +7,9 @@ import {
View,
} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import {atoms as a} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
type Props = {
@@ -20,7 +19,8 @@ type Props = {
}
export function Footer({altText, isAltExpanded, onToggleAltExpanded}: Props) {
const {_} = useLingui()
const {t: l} = useLingui()
const t = useTheme()
const insets = useSafeAreaInsets()
const isMomentumScrolling = useRef(false)
@@ -28,8 +28,14 @@ export function Footer({altText, isAltExpanded, onToggleAltExpanded}: Props) {
return (
<View
style={[styles.root, {paddingBottom: insets.bottom + 8}]}
pointerEvents="box-none">
style={[
a.absolute,
a.left_0,
a.right_0,
a.bottom_0,
a.pointer_events_box_none,
{paddingBottom: insets.bottom + 8},
]}>
<View style={[a.mx_md, styles.altWrap]}>
<ScrollView
scrollEnabled={isAltExpanded}
@@ -42,7 +48,7 @@ export function Footer({altText, isAltExpanded, onToggleAltExpanded}: Props) {
contentContainerStyle={[a.px_md, a.py_sm]}>
<Pressable
accessibilityRole="button"
accessibilityLabel={_(msg`Expand alt text`)}
accessibilityLabel={l`Expand alt text`}
accessibilityHint=""
onPress={() => {
if (isMomentumScrolling.current) return
@@ -55,7 +61,7 @@ export function Footer({altText, isAltExpanded, onToggleAltExpanded}: Props) {
<Text
emoji
selectable
style={[a.text_sm, styles.altText]}
style={[a.text_sm, {color: t.palette.white}]}
numberOfLines={isAltExpanded ? undefined : 3}>
{altText}
</Text>
@@ -67,18 +73,9 @@ export function Footer({altText, isAltExpanded, onToggleAltExpanded}: Props) {
}
const styles = StyleSheet.create({
root: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
},
altWrap: {
backgroundColor: 'rgba(0, 0, 0, 0.45)',
borderRadius: 12,
overflow: 'hidden',
},
altText: {
color: '#fff',
},
})
+10 -17
View File
@@ -1,7 +1,6 @@
import {StyleSheet, View} from 'react-native'
import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import {atoms as a} from '#/alf'
import {TimesLarge_Stroke2_Corner0_Rounded as CloseIcon} from '#/components/icons/Times'
@@ -24,36 +23,30 @@ export function Header({
imageCount,
activeIndex,
}: Props) {
const {_} = useLingui()
const {t: l} = useLingui()
const insets = useSafeAreaInsets()
return (
<View
style={[
styles.root,
a.absolute,
a.top_0,
a.left_0,
a.right_0,
a.flex_row,
a.justify_between,
a.align_center,
a.px_md,
a.pointer_events_box_none,
{paddingTop: insets.top + 8},
]}
pointerEvents="box-none">
]}>
<ImageMenu onPressShare={onPressShare} onPressSave={onPressSave} />
<PagerDots count={imageCount} activeIndex={activeIndex} />
<CircleChromeButton
icon={CloseIcon}
label={_(msg`Close image`)}
label={l`Close image`}
onPress={onRequestClose}
/>
</View>
)
}
const styles = StyleSheet.create({
root: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
},
})
+10 -10
View File
@@ -8,10 +8,9 @@ import Animated, {
withSpring,
withTiming,
} from 'react-native-reanimated'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import {atoms as a} from '#/alf'
import {android, atoms as a, ios} from '#/alf'
import {ArrowShareRight_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowShareRight'
import {type Props as IconProps} from '#/components/icons/common'
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid'
@@ -36,7 +35,7 @@ const SPRING_IN = {damping: 18, mass: 0.6, stiffness: 240}
const TIMING_OUT = {duration: 150}
export function ImageMenu({onPressShare, onPressSave}: Props) {
const {_} = useLingui()
const {t: l} = useLingui()
const triggerRef = useRef<View>(null)
const [isMounted, setIsMounted] = useState(false)
const [anchor, setAnchor] = useState<Anchor | null>(null)
@@ -71,7 +70,7 @@ export function ImageMenu({onPressShare, onPressSave}: Props) {
<CircleChromeButton
icon={DotsIcon}
iconStyle={{transform: [{rotate: '90deg'}]}}
label={_(msg`Image options`)}
label={l`Image options`}
onPress={open}
/>
</View>
@@ -83,21 +82,21 @@ export function ImageMenu({onPressShare, onPressSave}: Props) {
statusBarTranslucent>
<Pressable
accessibilityRole="button"
accessibilityLabel={_(msg`Close menu`)}
accessibilityLabel={l`Close menu`}
accessibilityHint=""
style={StyleSheet.absoluteFill}
style={[a.absolute, a.inset_0]}
onPress={close}
/>
{anchor && (
<MenuCard anchor={anchor} progress={progress}>
<MenuItem
icon={ShareIcon}
label={_(msg`Share image`)}
label={l`Share image`}
onPress={() => runAction(onPressShare)}
/>
<MenuItem
icon={DownloadIcon}
label={_(msg`Save image`)}
label={l`Save image`}
onPress={() => runAction(onPressSave)}
/>
</MenuCard>
@@ -126,6 +125,8 @@ function MenuCard({
style={[
a.absolute,
styles.card,
android({alignSelf: 'flex-start'}),
ios({width: MENU_WIDTH}),
{
top: anchor.y + anchor.height + GAP,
left: anchor.x,
@@ -162,7 +163,6 @@ function MenuItem({
const styles = StyleSheet.create({
card: {
width: MENU_WIDTH,
padding: 8,
borderRadius: 16,
borderWidth: 0.5,
+60 -14
View File
@@ -1,5 +1,10 @@
import {useCallback, useMemo} from 'react'
import {type GestureResponderEvent, Linking} from 'react-native'
import {
type GestureResponderEvent,
Linking,
type NativeSyntheticEvent,
type TargetedEvent,
} from 'react-native'
import {sanitizeUrl} from '@braintree/sanitize-url'
import {
type LinkProps as RNLinkProps,
@@ -78,6 +83,15 @@ type BaseLinkProps = {
* Whether the link should be opened through the redirect proxy.
*/
shouldProxy?: boolean
/**
* Web only
*/
onMouseEnter?: () => void
/**
* Web only
*/
onMouseLeave?: () => void
}
export function useLink({
@@ -322,7 +336,7 @@ export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps &
TextStyleProp &
Pick<TextProps, 'selectable' | 'numberOfLines' | 'emoji'> &
Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
Pick<ButtonProps, 'label' | 'accessibilityHint' | 'onFocus' | 'onBlur'> & {
disableUnderline?: boolean
title?: TextProps['title']
overridePresentation?: boolean
@@ -360,9 +374,9 @@ export function InlineLinkText({
shouldProxy: shouldProxy,
})
const {
state: hovered,
onIn: onHoverIn,
onOut: onHoverOut,
state: interacted,
onIn: onInteract,
onOut: onInteractOut,
} = useInteractionState()
const flattenedStyle = flatten(style) || {}
@@ -374,7 +388,7 @@ export function InlineLinkText({
{...rest}
style={[
{color: t.palette.primary_500},
hovered &&
interacted &&
!disableUnderline && {
...web({
outline: 0,
@@ -388,8 +402,24 @@ export function InlineLinkText({
role="link"
onPress={download ? undefined : onPress}
onLongPress={onLongPress}
onMouseEnter={onHoverIn}
onMouseLeave={onHoverOut}
{...web({
onMouseEnter: () => {
rest.onMouseEnter?.()
onInteract()
},
onMouseLeave: () => {
rest.onMouseLeave?.()
onInteractOut()
},
})}
onFocus={(e: NativeSyntheticEvent<TargetedEvent>) => {
rest.onFocus?.(e)
onInteract()
}}
onBlur={(e: NativeSyntheticEvent<TargetedEvent>) => {
rest.onBlur?.(e)
onInteractOut()
}}
accessibilityRole="link"
href={href}
{...web({
@@ -436,9 +466,9 @@ export function SimpleInlineLinkText({
}) {
const t = useTheme()
const {
state: hovered,
onIn: onHoverIn,
onOut: onHoverOut,
state: interacted,
onIn: onInteract,
onOut: onInteractOut,
} = useInteractionState()
const flattenedStyle = flatten(style) || {}
const isExternal = isExternalUrl(to)
@@ -462,7 +492,7 @@ export function SimpleInlineLinkText({
{...rest}
style={[
{color: t.palette.primary_500},
hovered &&
interacted &&
!disableUnderline && {
...web({
outline: 0,
@@ -475,8 +505,24 @@ export function SimpleInlineLinkText({
]}
role="link"
onPress={onPress}
onMouseEnter={onHoverIn}
onMouseLeave={onHoverOut}
{...web({
onMouseEnter: () => {
rest.onMouseEnter?.()
onInteract()
},
onMouseLeave: () => {
rest.onMouseLeave?.()
onInteractOut()
},
})}
onFocus={(e: NativeSyntheticEvent<TargetedEvent>) => {
rest.onFocus?.(e)
onInteract()
}}
onBlur={(e: NativeSyntheticEvent<TargetedEvent>) => {
rest.onBlur?.(e)
onInteractOut()
}}
accessibilityRole="link"
href={href}
{...web({
+1
View File
@@ -98,6 +98,7 @@ export function Outer({
}: React.PropsWithChildren<{
showCancel?: boolean
style?: StyleProp<ViewStyle>
onCloseAutoFocus?: (event: Event) => void
}>) {
const context = useMenuContext()
const {_} = useLingui()
+5
View File
@@ -181,9 +181,13 @@ export function Trigger({
export function Outer({
children,
style,
onCloseAutoFocus,
}: React.PropsWithChildren<{
showCancel?: boolean
style?: StyleProp<ViewStyle>
onCloseAutoFocus?: React.ComponentProps<
typeof DropdownMenu.Content
>['onCloseAutoFocus']
}>) {
const t = useTheme()
const {reduceMotionEnabled} = useA11y()
@@ -195,6 +199,7 @@ export function Outer({
collisionPadding={{left: 5, right: 5, bottom: 5}}
loop
aria-label="Test"
onCloseAutoFocus={onCloseAutoFocus}
className="dropdown-menu-transform-origin dropdown-menu-constrain-size">
<View
style={[
@@ -0,0 +1,100 @@
import {Fragment, type ReactNode} from 'react'
import {View} from 'react-native'
import {type AppBskyEmbedExternal, AtUri} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {makeProfileLink} from '#/lib/routes/links'
import {toNiceDomain} from '#/lib/strings/url-helpers'
import {atoms as a, useTheme} from '#/alf'
import {StandardSite} from '#/components/icons/community/StandardSite'
import {InlineLinkText} from '#/components/Link'
import {
matchStandardSitePublisher,
matchStandardSitePublisherByUri,
} from '#/components/Post/Embed/StandardSiteEmbed/publishers'
import {
isStandardSiteDocumentUri,
isStandardSitePublicationUri,
} from '#/components/Post/Embed/StandardSiteEmbed/utils'
import {Text} from '#/components/Typography'
export function StandardSiteMetaRow({
type = 'document',
view,
}: {
type?: 'document' | 'publication'
view: AppBskyEmbedExternal.ViewExternal
}) {
const t = useTheme()
const {t: l} = useLingui()
const highlightedPublisher = !!matchStandardSitePublisher(view)
const didsFromRecords =
view.associatedRefs
?.filter(
type === 'document'
? isStandardSiteDocumentUri
: isStandardSitePublicationUri,
)
.map(ref => new AtUri(ref.uri).host) || []
// atm should only be one docment
const authorDid = didsFromRecords.at(0)
const authorProfile = authorDid
? view.associatedProfiles?.find(p => p.did === authorDid)
: undefined
const articleDomain = toNiceDomain(view.uri)
const articlePublisher = matchStandardSitePublisherByUri(view.uri)
const DomainIcon = articlePublisher?.Icon ?? StandardSite
const metaTextStyle = [
a.text_xs,
a.leading_snug,
t.atoms.text_contrast_medium,
]
const items: {key: string; node: ReactNode}[] = []
if (!highlightedPublisher) {
items.push({
key: 'domain',
node: (
<View style={[a.flex_row, a.align_center]}>
<DomainIcon size="sm" fill={t.atoms.text_contrast_medium.color} />
<Text numberOfLines={1} style={metaTextStyle}>
{articleDomain}
</Text>
</View>
),
})
}
if (authorProfile) {
items.push({
key: 'author',
node: (
<Text numberOfLines={1} style={metaTextStyle}>
<Trans>
by{' '}
<InlineLinkText
label={l`View @${authorProfile.handle}'s profile`}
to={makeProfileLink(authorProfile)}
style={[metaTextStyle, a.pointer_events_auto]}>
@{authorProfile.handle}
</InlineLinkText>
</Trans>
</Text>
),
})
}
if (items.length === 0) return null
return (
<View style={[a.flex_row, a.align_center, a.gap_xs, a.z_10]}>
{items.map((item, i) => (
<Fragment key={item.key}>
{i > 0 && <Text style={metaTextStyle}></Text>}
{item.node}
</Fragment>
))}
</View>
)
}
@@ -0,0 +1,67 @@
import {type AppBskyEmbedExternal} from '@atproto/api'
import {Context, useAlf, utils} from '#/alf'
/**
* Overrides only the values needed for `secondary_inverted` buttons atm.
*
* Renders the alf Context directly (rather than nesting a ThemeProvider) so
* that font-scale and other parent state stay live in the subtree.
*/
export function StandardSiteThemeProvider({
view,
children,
}: {
view: AppBskyEmbedExternal.ViewExternal
children: React.ReactNode
}) {
const alf = useAlf()
const {accentRGB, accentForegroundRGB} = view.source?.theme || {}
if (!accentRGB || !accentForegroundRGB) return children
const accent = utils.rgbToHex(accentRGB.r, accentRGB.g, accentRGB.b)
const accentForeground = utils.rgbToHex(
accentForegroundRGB.r,
accentForegroundRGB.g,
accentForegroundRGB.b,
)
const atomsOverride = {
text_inverted: {color: accentForeground},
}
const paletteOverride = {
contrast_975: utils.darken(accent, 5), // hover
contrast_900: accent, // bg
contrast_600: utils.lighten(accent, 5), // disabled bg
contrast_300: accentForeground, // disabled text
}
const themes = {
...alf.themes,
lightPalette: {...alf.themes.lightPalette, ...paletteOverride},
darkPalette: {...alf.themes.darkPalette, ...paletteOverride},
dimPalette: {...alf.themes.dimPalette, ...paletteOverride},
light: {
...alf.themes.light,
atoms: {...alf.themes.light.atoms, ...atomsOverride},
palette: {...alf.themes.light.palette, ...paletteOverride},
},
dark: {
...alf.themes.dark,
atoms: {...alf.themes.dark.atoms, ...atomsOverride},
palette: {...alf.themes.dark.palette, ...paletteOverride},
},
dim: {
...alf.themes.dim,
atoms: {...alf.themes.dim.atoms, ...atomsOverride},
palette: {...alf.themes.dim.palette, ...paletteOverride},
},
}
const value = {
...alf,
themes,
theme: themes[alf.themeName],
}
return <Context.Provider value={value}>{children}</Context.Provider>
}
@@ -0,0 +1,555 @@
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {Image} from 'expo-image'
import {type AppBskyEmbedExternal, AtUri} from '@atproto/api'
import {plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react/macro'
import {useHaptics} from '#/lib/haptics'
import {shareUrl} from '#/lib/sharing'
import {niceDate} from '#/lib/strings/time'
import {toNiceDomain} from '#/lib/strings/url-helpers'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, utils, web} from '#/alf'
import {ButtonIcon, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock'
import {Link} from '#/components/Link'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {matchStandardSitePublisher} from '#/components/Post/Embed/StandardSiteEmbed/publishers'
import {StandardSiteMetaRow} from '#/components/Post/Embed/StandardSiteEmbed/StandardSiteMetaRow'
import {StandardSiteThemeProvider} from '#/components/Post/Embed/StandardSiteEmbed/StandardSiteThemeProvider'
import {isStandardSitePublicationEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
import {Text} from '#/components/Typography'
import {IS_NATIVE} from '#/env'
export type ThemeColors = {
custom: boolean
accent: string
accentForeground: string
}
const PUBLICATION_AVATAR_STYLE = {
borderRadius: a.rounded_sm.borderRadius,
}
export const StandardSiteEmbed = ({
view,
onOpen,
style,
hideSubscribe,
}: {
view: AppBskyEmbedExternal.ViewExternal
onOpen?: () => void
style?: StyleProp<ViewStyle>
hideSubscribe?: boolean
}) => {
const {t: l, i18n} = useLingui()
const t = useTheme()
const playHaptic = useHaptics()
const niceUrl = toNiceDomain(view.uri)
const imageUri = view.thumb
const hasMedia = Boolean(imageUri)
const isStandard = view.associatedRefs?.some(ref =>
new AtUri(ref.uri).collection.startsWith('site.standard.'),
)
const isStandardPublication = isStandardSitePublicationEmbed(view)
let themeColors: ThemeColors = {
custom: false,
accent: t.atoms.text.color,
accentForeground: t.atoms.text_inverted.color,
}
const {accentRGB, accentForegroundRGB} = view.source?.theme || {}
if (accentRGB && accentForegroundRGB) {
themeColors = {
custom: true,
accent: utils.rgbToHex(accentRGB.r, accentRGB.g, accentRGB.b),
accentForeground: utils.rgbToHex(
accentForegroundRGB.r,
accentForegroundRGB.g,
accentForegroundRGB.b,
),
}
}
const {
state: interacted,
onIn: onInteract,
onOut: onInteractOut,
} = useInteractionState()
const onPress = () => {
playHaptic('Light')
onOpen?.()
}
const onLongPress = () => {
if (view.uri && IS_NATIVE) {
playHaptic('Heavy')
shareUrl(view.uri)
}
}
if (isStandardPublication) {
return (
<PublicationCard
hideSubscribe={hideSubscribe}
view={view}
onPress={onPress}
onLongPress={onLongPress}
style={style}
themeColors={themeColors}
/>
)
}
return (
<View
style={[
a.flex_col,
a.rounded_lg,
a.overflow_hidden,
a.w_full,
a.border,
interacted ? t.atoms.border_contrast_high : t.atoms.border_contrast_low,
style,
]}>
<Link
shouldProxy
to={view.uri}
label={view.title || l`Open link to ${niceUrl}`}
onPress={onPress}
onLongPress={onLongPress}
style={[a.absolute, a.inset_0, a.z_10]}
{...web({
onMouseEnter: onInteract,
onMouseLeave: onInteractOut,
})}
onFocus={onInteract}
onBlur={onInteractOut}>
<></>
</Link>
<View style={[a.w_full, a.z_10, a.pointer_events_none]}>
{imageUri ? (
<Image
style={[a.aspect_card]}
source={{uri: imageUri}}
accessibilityIgnoresInvertColors
loading="lazy"
/>
) : undefined}
<View
style={[
a.flex_1,
a.pt_sm,
hasMedia && a.border_t,
interacted
? t.atoms.border_contrast_high
: t.atoms.border_contrast_low,
{gap: 3},
isStandard && a.pt_md,
]}>
<View
style={[
a.pb_xs,
a.px_md,
{gap: 3},
isStandard && [{gap: 5}, a.pb_sm],
]}>
<Text
emoji
numberOfLines={3}
style={[
a.text_md,
a.font_semi_bold,
a.leading_snug,
isStandard && [a.text_lg, a.font_bold],
]}>
{view.title}
</Text>
{view.description ? (
<Text
emoji
numberOfLines={view.thumb ? 2 : 4}
style={[a.text_sm, a.leading_snug]}>
{view.description}
</Text>
) : undefined}
{isStandard && (view.createdAt || view.readingTime) && (
<View
style={[a.flex_row, a.align_center, a.gap_md, {paddingTop: 2}]}>
{view.createdAt && (
<Text
style={[
a.text_xs,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
{niceDate(i18n, view.createdAt, 'long', 'none')}
</Text>
)}
{view.readingTime && (
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
<Clock size="xs" style={t.atoms.text_contrast_medium} />
<Text
style={[
a.text_xs,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
{l({
message: plural(view.readingTime, {
one: '#m',
other: '#m',
}),
comment: `How long it takes to read an article, in minutes. Displayed in a short form, e.g. "5m" for 5 minutes.`,
})}
</Text>
</View>
)}
</View>
)}
</View>
</View>
</View>
<View style={[a.z_20]}>
<View style={[a.px_md]}>
<Divider />
</View>
{view.source ? (
<PublicationFooter
view={view}
onPress={onPress}
onLongPress={onLongPress}
themeColors={themeColors}
hideSubscribe={hideSubscribe}
/>
) : (
<View style={[a.px_md, a.py_sm, a.pointer_events_none]}>
<StandardSiteMetaRow view={view} />
</View>
)}
</View>
</View>
)
}
export function PublicationCard({
view,
hideSubscribe,
onPress,
onLongPress,
themeColors,
style,
}: {
view: AppBskyEmbedExternal.ViewExternal
hideSubscribe?: boolean
onPress?: () => void
onLongPress?: () => void
themeColors: ThemeColors
style?: StyleProp<ViewStyle>
}) {
const t = useTheme()
const {t: l} = useLingui()
const {gtPhone} = useBreakpoints()
const {
state: interacted,
onIn: onInteract,
onOut: onInteractOut,
} = useInteractionState()
if (!view.source) return null
return (
<View
style={[
a.rounded_lg,
a.overflow_hidden,
a.w_full,
a.border,
a.p_md,
interacted ? t.atoms.border_contrast_high : t.atoms.border_contrast_low,
style,
]}>
<Link
shouldProxy
to={view.source.uri}
label={
view.source.title ? l`View ${view.source.title}` : l`View publication`
}
onPress={onPress}
onLongPress={onLongPress}
{...web({
onMouseEnter: onInteract,
onMouseLeave: onInteractOut,
})}
onFocus={onInteract}
onBlur={onInteractOut}
style={[a.absolute, a.inset_0]}>
<></>
</Link>
<View
style={[
a.flex_1,
a.align_center,
a.justify_between,
a.gap_md,
a.pointer_events_none,
gtPhone && [a.flex_row, a.gap_sm],
]}>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.gap_sm,
gtPhone && a.flex_1,
]}>
<PublicationIcon
view={view}
size={40}
interacted={interacted}
themeColors={themeColors}
/>
<View style={[a.flex_1, a.gap_2xs]}>
<Text
numberOfLines={1}
style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
{view.source?.title}
</Text>
<StandardSiteMetaRow type="publication" view={view} />
</View>
</View>
{!hideSubscribe && gtPhone && (
<SubscribeButton
view={view}
style={[!gtPhone && [a.w_full, a.justify_center]]}
onPress={onPress}
onLongPress={onLongPress}
/>
)}
</View>
<View style={[a.pointer_events_none]}>
{view.description && (
<View style={[a.pt_sm]}>
<Text style={[a.text_sm, a.leading_snug]} numberOfLines={3}>
{view.description}
</Text>
</View>
)}
{!hideSubscribe && !gtPhone && (
<View style={[view.description && a.pt_sm]}>
<SubscribeButton
view={view}
style={[!gtPhone && [a.w_full, a.justify_center]]}
onPress={onPress}
onLongPress={onLongPress}
/>
</View>
)}
</View>
</View>
)
}
export function SubscribeButton({
view,
onPress,
onLongPress,
style,
}: {
view: AppBskyEmbedExternal.ViewExternal
onPress?: () => void
onLongPress?: () => void
style?: StyleProp<ViewStyle>
}) {
const {t: l} = useLingui()
const highlightedPublisher = matchStandardSitePublisher(view)
const cta = highlightedPublisher
? l`Subscribe on ${highlightedPublisher.name}`
: l`View publication`
if (!view.source) return null
const publicationTitle = view.source.title
const label = highlightedPublisher
? publicationTitle
? l`Subscribe to ${publicationTitle} on ${highlightedPublisher.name}`
: l`Subscribe on ${highlightedPublisher.name}`
: publicationTitle
? l`View ${publicationTitle}`
: l`View publication`
return (
<StandardSiteThemeProvider view={view}>
<Link
shouldProxy
to={view.source.uri}
label={label}
size="small"
color="secondary_inverted"
style={[style, a.gap_sm, a.pointer_events_auto]}
onPress={onPress}
onLongPress={onLongPress}>
{highlightedPublisher ? (
<>
<View style={[a.flex_row, a.align_center, {gap: 7}]}>
<ButtonIcon icon={highlightedPublisher.Icon} size="lg" />
{/*<ButtonText>|</ButtonText>*/}
</View>
<ButtonText>{cta}</ButtonText>
</>
) : (
<ButtonText>{cta}</ButtonText>
)}
</Link>
</StandardSiteThemeProvider>
)
}
function PublicationIcon({
view,
size,
themeColors,
}: {
view: AppBskyEmbedExternal.ViewExternal
size: number
interacted?: boolean
themeColors: ThemeColors
}) {
if (!view.source) return null
return view.source?.icon ? (
<View>
<UserAvatar
noBorder
type="labeler"
size={size}
avatar={view.source.icon}
extraAviStyle={PUBLICATION_AVATAR_STYLE}
/>
<MediaInsetBorder opaque style={[a.rounded_sm]} />
</View>
) : (
<View
style={[
a.align_center,
a.justify_center,
a.rounded_sm,
{
width: size,
height: size,
backgroundColor: themeColors.accent,
},
]}>
<Text
emoji
style={[a.text_xl, a.font_bold, {color: themeColors.accentForeground}]}>
{[...view.source.title][0] ?? ''}
</Text>
<MediaInsetBorder opaque style={[a.rounded_sm]} />
</View>
)
}
export function PublicationFooter({
view,
hideSubscribe,
onPress,
onLongPress,
themeColors,
}: {
view: AppBskyEmbedExternal.ViewExternal
hideSubscribe?: boolean
themeColors: ThemeColors
onPress?: () => void
onLongPress?: () => void
}) {
const t = useTheme()
const {t: l} = useLingui()
const {gtPhone} = useBreakpoints()
const {
state: interacted,
onIn: onInteract,
onOut: onInteractOut,
} = useInteractionState()
if (!view.source) return null
return (
<View
style={[
a.flex_1,
a.align_center,
a.justify_between,
a.p_md,
a.gap_md,
gtPhone && [a.flex_row, a.gap_sm],
]}
testID="publication-embed-footer">
<Link
shouldProxy
to={view.source.uri}
label={
view.source.title ? l`View ${view.source.title}` : l`View publication`
}
onPress={onPress}
onLongPress={onLongPress}
style={[a.absolute, a.inset_0, web({outline: 0})]}
{...web({
onMouseEnter: onInteract,
onMouseLeave: onInteractOut,
})}
onFocus={onInteract}
onBlur={onInteractOut}>
<></>
</Link>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.gap_sm,
gtPhone && a.flex_1,
a.pointer_events_none,
]}>
<PublicationIcon
view={view}
size={32}
interacted={interacted}
themeColors={themeColors}
/>
<View style={[a.flex_1, a.gap_2xs]}>
<Text
numberOfLines={1}
style={[
a.text_sm,
a.font_medium,
t.atoms.text,
interacted && a.underline,
]}>
{view.source?.title}
</Text>
<StandardSiteMetaRow type="publication" view={view} />
</View>
</View>
{!hideSubscribe && (
<SubscribeButton
view={view}
style={[a.z_10, !gtPhone && [a.w_full, a.justify_center]]}
onPress={onPress}
onLongPress={onLongPress}
/>
)}
</View>
)
}
@@ -0,0 +1,52 @@
import {type AppBskyEmbedExternal} from '@atproto/api'
import {Leaflet} from '#/components/icons/community/Leaflet'
import {Offprint} from '#/components/icons/community/Offprint'
import {Pckt} from '#/components/icons/community/Pckt'
export type StandardSitePublisher = {
host: string
name: string
Icon: typeof Leaflet
}
const STANDARD_SITE_PUBLISHERS: StandardSitePublisher[] = [
{host: 'leaflet.pub', name: 'Leaflet', Icon: Leaflet},
{host: 'pckt.blog', name: 'pckt', Icon: Pckt},
{host: 'offprint.app', name: 'Offprint', Icon: Offprint},
]
function hostFromUri(uri: string | undefined): string | null {
try {
return new URL(uri || '').host
} catch {
return null
}
}
export function getStandardSitePublisherHost(
view: AppBskyEmbedExternal.ViewExternal,
): string | null {
return hostFromUri(view.source?.uri)
}
export function hostMatches(host: string, target: string): boolean {
return host === target || host.endsWith('.' + target)
}
function matchByHost(host: string | null): StandardSitePublisher | null {
if (!host) return null
return STANDARD_SITE_PUBLISHERS.find(p => hostMatches(host, p.host)) ?? null
}
export function matchStandardSitePublisher(
view: AppBskyEmbedExternal.ViewExternal,
): StandardSitePublisher | null {
return matchByHost(getStandardSitePublisherHost(view))
}
export function matchStandardSitePublisherByUri(
uri: string | undefined,
): StandardSitePublisher | null {
return matchByHost(hostFromUri(uri))
}
@@ -0,0 +1,70 @@
import {type AppBskyEmbedExternal} from '@atproto/api'
import {isStandardSiteEmbed, isStandardSitePublicationEmbed} from './utils'
function makeView(
partial: Record<string, unknown>,
): AppBskyEmbedExternal.ViewExternal {
return {
uri: 'https://example.com/post',
title: 'title',
description: 'description',
...partial,
}
}
describe('isStandardSiteEmbed', () => {
it('returns true when any associated ref is in the site.standard.* namespace', () => {
const view = makeView({
associatedRefs: [{uri: 'at://did:plc:abc/site.standard.publication/foo'}],
})
expect(isStandardSiteEmbed(view)).toBe(true)
})
it('returns false when no associated refs are in the site.standard.* namespace', () => {
const view = makeView({
associatedRefs: [{uri: 'at://did:plc:abc/app.bsky.feed.post/foo'}],
})
expect(isStandardSiteEmbed(view)).toBe(false)
})
it('returns falsy when associatedRefs is missing', () => {
expect(isStandardSiteEmbed(makeView({}))).toBeFalsy()
})
})
describe('isStandardSitePublicationEmbed', () => {
it('returns true with at least one publication ref and no document refs', () => {
const view = makeView({
associatedRefs: [{uri: 'at://did:plc:abc/site.standard.publication/foo'}],
})
expect(isStandardSitePublicationEmbed(view)).toBe(true)
})
it('returns false when any ref is a document', () => {
const view = makeView({
associatedRefs: [
{uri: 'at://did:plc:abc/site.standard.publication/foo'},
{uri: 'at://did:plc:abc/site.standard.document/bar'},
],
})
expect(isStandardSitePublicationEmbed(view)).toBe(false)
})
it('returns false when there are no publication refs', () => {
const view = makeView({
associatedRefs: [{uri: 'at://did:plc:abc/site.standard.other/foo'}],
})
expect(isStandardSitePublicationEmbed(view)).toBe(false)
})
it('returns falsy for an empty associatedRefs array', () => {
expect(isStandardSitePublicationEmbed(makeView({associatedRefs: []}))).toBe(
false,
)
})
it('returns falsy when associatedRefs is missing', () => {
expect(isStandardSitePublicationEmbed(makeView({}))).toBeFalsy()
})
})
@@ -0,0 +1,36 @@
import {
type AppBskyEmbedExternal,
AtUri,
type ComAtprotoRepoStrongRef,
} from '@atproto/api'
export function isStandardSiteDocumentUri(ref: ComAtprotoRepoStrongRef.Main) {
return new AtUri(ref.uri).collection.startsWith('site.standard.document')
}
export function isStandardSitePublicationUri(
ref: ComAtprotoRepoStrongRef.Main,
) {
return new AtUri(ref.uri).collection.startsWith('site.standard.publication')
}
export function isStandardSiteUri(ref: ComAtprotoRepoStrongRef.Main) {
return new AtUri(ref.uri).collection.startsWith('site.standard.')
}
export function isStandardSiteEmbed(view: AppBskyEmbedExternal.ViewExternal) {
return view.associatedRefs?.some(ref => isStandardSiteUri(ref))
}
export function isStandardSitePublicationEmbed(
view: AppBskyEmbedExternal.ViewExternal,
) {
return (
view.associatedRefs?.some(
ref => new AtUri(ref.uri).collection === 'site.standard.publication',
) &&
view.associatedRefs.every(
ref => new AtUri(ref.uri).collection !== 'site.standard.document',
)
)
}
@@ -103,6 +103,34 @@ export class HLSUnsupportedError extends Error {
}
}
// Bluesky serves HLS as MPEG-TS with H.264 + AAC. `Hls.isSupported()` is loose
// (true if MSE supports *any* of {H.264, AV1, VP9} OR *any* of {AAC, FLAC}),
// so on Linux boxes missing H.264 (e.g. no ubuntu-restricted-extras, sandboxed
// Firefox snap) it returns true and playback fails later on segment append.
// Use Baseline 3.0 to match hls.js's own probe - it's the most universal H.264
// profile, so if it isn't supported, no H.264 is.
// Mirror hls.js's `getMediaSource` lookup (ManagedMediaSource on modern iOS,
// then MediaSource, then WebKitMediaSource) so we probe the same constructor
// it will actually use for playback.
function canPlayBskyVideoCodecs(): boolean {
if (typeof self === 'undefined') return false
const globalSelf = self as typeof self & {
ManagedMediaSource?: typeof MediaSource
WebKitMediaSource?: typeof MediaSource
}
const mediaSource =
globalSelf.ManagedMediaSource ||
globalSelf.MediaSource ||
globalSelf.WebKitMediaSource
if (!mediaSource || typeof mediaSource.isTypeSupported !== 'function') {
return false
}
return (
mediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E"') &&
mediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"')
)
}
export class VideoNotFoundError extends Error {
constructor() {
super('Video not found')
@@ -228,7 +256,7 @@ function useHLS({
useEffect(() => {
if (!videoRef.current) return
if (!Hls) return
if (!Hls.isSupported()) {
if (!Hls.isSupported() || !canPlayBskyVideoCodecs()) {
throw new HLSUnsupportedError()
}
@@ -233,7 +233,7 @@ function VideoError({error, retry}: {error: unknown; retry: () => void}) {
} else if (error instanceof HLSUnsupportedError) {
showRetryButton = false
text = _(
msg`Your browser does not support the video format. Please try a different browser.`,
msg`This video cant be played on your device. Your browser or system may be missing the required video codecs (H.264/AAC).`,
)
} else {
text = _(msg`An error occurred while loading the video. Please try again.`)
+14
View File
@@ -22,6 +22,8 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
import {GalleryBleed} from '#/components/images/Gallery'
import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {StandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed'
import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
import {RichText} from '#/components/RichText'
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {SubtleHover} from '#/components/SubtleHover'
@@ -95,6 +97,18 @@ function MediaEmbed({
)
}
case 'link': {
if (isStandardSiteEmbed(embed.view.external)) {
return (
<ContentHider
modui={rest.moderation?.ui('contentMedia')}
activeStyle={[a.mt_sm]}>
<StandardSiteEmbed
view={embed.view.external}
style={[a.mt_sm, rest.style]}
/>
</ContentHider>
)
}
return (
<ContentHider
modui={rest.moderation?.ui('contentMedia')}
@@ -36,7 +36,10 @@ export function RecentChats({
const ax = useAnalytics()
const control = useDialogContext()
const {currentAccount} = useSession()
const {data} = useListConvosQuery({status: 'accepted'})
const {data} = useListConvosQuery({
status: 'accepted',
lockStatus: 'unlocked',
})
const convos = data?.pages[0]?.convos?.slice(0, 10)
const moderationOpts = useModerationOpts()
const navigation = useNavigation<NavigationProp>()
@@ -114,6 +114,7 @@ export function SearchablePeopleList({
const {data: convos} = useListConvosQuery({
enabled: showRecentConvos,
status: 'accepted',
lockStatus: 'unlocked',
})
const items = useMemo(() => {
+7 -3
View File
@@ -18,7 +18,7 @@ import {type ListMethods} from '#/view/com/util/List'
import {android, atoms as a, native, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {canBeMessaged, type ConvoWithDetails} from '#/components/dms/util'
import {canBeAddedToGroup, type ConvoWithDetails} from '#/components/dms/util'
import * as Toggle from '#/components/forms/Toggle'
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
@@ -190,7 +190,9 @@ export function AddMembersFlow({
}
_items.sort(item => {
return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1
return item.type === 'profile' && canBeAddedToGroup(item.profile)
? -1
: 1
})
}
} else {
@@ -206,7 +208,9 @@ export function AddMembersFlow({
}
_items.sort(item => {
return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1
return item.type === 'profile' && canBeAddedToGroup(item.profile)
? -1
: 1
})
} else {
for (let i = 0; i < 10; i++) {
-101
View File
@@ -1,101 +0,0 @@
import {useCallback, useMemo, useState} from 'react'
import {Pressable, View} from 'react-native'
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {ScaleAndFadeIn} from '#/lib/custom-animations/ScaleAndFade'
import {ShrinkAndPop} from '#/lib/custom-animations/ShrinkAndPop'
import {useHaptics} from '#/lib/haptics'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {IS_WEB} from '#/env'
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
let lastIndex = 0
export function ChatEmptyPill() {
const t = useTheme()
const {_} = useLingui()
const playHaptic = useHaptics()
const [promptIndex, setPromptIndex] = useState(lastIndex)
const scale = useSharedValue(1)
const prompts = useMemo(() => {
return [
_(msg`Say hello!`),
_(msg`Share your favorite feed!`),
_(msg`Tell a joke!`),
_(msg`Share a fun fact!`),
_(msg`Share a cool story!`),
_(msg`Send a neat website!`),
_(msg`Clip 🐴 clop 🐴`),
]
}, [_])
const onPressIn = useCallback(() => {
if (IS_WEB) return
scale.set(() => withTiming(1.075, {duration: 100}))
}, [scale])
const onPressOut = useCallback(() => {
if (IS_WEB) return
scale.set(() => withTiming(1, {duration: 100}))
}, [scale])
const onPress = useCallback(() => {
runOnJS(playHaptic)()
let randomPromptIndex = Math.floor(Math.random() * prompts.length)
while (randomPromptIndex === lastIndex) {
randomPromptIndex = Math.floor(Math.random() * prompts.length)
}
setPromptIndex(randomPromptIndex)
lastIndex = randomPromptIndex
}, [playHaptic, prompts.length])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.get()}],
}))
return (
<View
style={[
a.absolute,
a.w_full,
a.z_10,
a.align_center,
{
top: -50,
},
]}>
<AnimatedPressable
style={[
a.px_xl,
a.py_md,
a.rounded_full,
t.atoms.bg_contrast_25,
a.align_center,
animatedStyle,
]}
entering={ScaleAndFadeIn}
exiting={ShrinkAndPop}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}>
<Text
style={[a.font_semi_bold, a.pointer_events_none]}
selectable={false}
emoji>
{prompts[promptIndex]}
</Text>
</AnimatedPressable>
</View>
)
}
+13 -1
View File
@@ -68,7 +68,19 @@ function MenuInner({
return expanded ? (
<EmojiPicker.Picker keepOpenWhenShiftHeld={false} />
) : (
<Menu.Outer style={[a.rounded_full]}>
<Menu.Outer
style={[a.rounded_full]}
onCloseAutoFocus={evt => {
// If something has already taken focus (e.g. emoji-mart's search
// input when swapping to the full picker), don't let Radix restore
// focus to the trigger and steal it back.
if (
document.activeElement &&
document.activeElement !== document.body
) {
evt.preventDefault()
}
}}>
<View style={[a.flex_row, a.gap_xs]}>
{['❤️', '👍', '😆', '👀', '😢'].map(emoji => {
const alreadyReacted = hasAlreadyReacted(
+7 -5
View File
@@ -20,7 +20,7 @@ import {type ListMethods} from '#/view/com/util/List'
import {android, atoms as a, native, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {canBeMessaged} from '#/components/dms/util'
import {canBeAddedToGroup, canBeMessaged} from '#/components/dms/util'
import * as TextField from '#/components/forms/TextField'
import * as Toggle from '#/components/forms/Toggle'
import {
@@ -246,6 +246,8 @@ export function InitiateChatFlow({
const items = useMemo(() => {
let _items: Item[] = []
const checker =
chatState === ChatState.NEW_GROUP_CHAT ? canBeAddedToGroup : canBeMessaged
if (isError) {
_items.push({
@@ -276,7 +278,7 @@ export function InitiateChatFlow({
}
_items = _items.sort(item => {
return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1
return item.type === 'profile' && checker(item.profile) ? -1 : 1
})
}
} else {
@@ -299,7 +301,7 @@ export function InitiateChatFlow({
}
_items = _items.sort(item => {
return item.type === 'profile' && canBeMessaged(item.profile) ? -1 : 1
return item.type === 'profile' && checker(item.profile) ? -1 : 1
})
} else {
_items.push(...placeholders)
@@ -825,7 +827,7 @@ function GroupChatMemberProfileCard({
moderationOpts: ModerationOpts
}) {
const t = useTheme()
const enabled = canBeMessaged(profile)
const enabled = canBeAddedToGroup(profile)
const handle = sanitizeHandle(profile.handle, '@')
return (
@@ -845,7 +847,7 @@ function GroupChatMemberProfileCard({
<Text
style={[a.leading_snug, t.atoms.text_contrast_high]}
numberOfLines={2}>
<Trans>{handle} cant be messaged</Trans>
<Trans>{handle} cant be added</Trans>
</Text>
)}
</View>
+40 -37
View File
@@ -8,8 +8,6 @@ import {
type ViewStyle,
} from 'react-native'
import Animated, {
FadeIn,
FadeOut,
LayoutAnimationConfig,
LinearTransition,
useAnimatedStyle,
@@ -70,7 +68,6 @@ function isWithinClusterBoundary({
direction: 'prev' | 'next'
}): boolean {
if (!isFromSameSender) return true
if (isPending && adjacentMessage) return false
if (ChatBskyConvoDefs.isMessageView(adjacentMessage)) {
const thisDate = new Date(currentSentAt)
const adjDate = new Date(adjacentMessage.sentAt)
@@ -78,7 +75,10 @@ function isWithinClusterBoundary({
direction === 'next'
? adjDate.getTime() - thisDate.getTime()
: thisDate.getTime() - adjDate.getTime()
return diff > CLUSTERED_MESSAGE_THRESHOLD_MS
const isOutsideThreshold = diff > CLUSTERED_MESSAGE_THRESHOLD_MS
// For pending messages, still check the time threshold
if (isPending) return isOutsideThreshold
return isOutsideThreshold
}
return true
}
@@ -217,6 +217,7 @@ let MessageItem = ({
const avatar =
profile && moderationOpts ? (
<Link
style={[a.rounded_full]}
label={l`${createSanitizedDisplayName(profile)}s avatar`}
accessibilityHint={l`Opens this profile`}
to={makeProfileLink({
@@ -380,21 +381,19 @@ let MessageItem = ({
)
const messageInset = platform<ViewStyle | undefined>({
ios: isFromSelf ? a.mr_md : isGroupChat ? a.ml_md : a.ml_sm,
android: isFromSelf ? a.mr_sm : isGroupChat ? a.ml_sm : undefined,
web: isFromSelf ? a.mr_lg : isGroupChat ? a.ml_lg : undefined,
android: a.mx_sm,
ios: a.mx_md,
web: a.mx_lg,
})
return (
<>
<LayoutAnimationConfig skipExiting skipEntering>
{hasLargeGapFromPrev && (
<Animated.View entering={native(FadeIn)} exiting={native(FadeOut)}>
<DateDivider date={message.sentAt} />
</Animated.View>
)}
</LayoutAnimationConfig>
<View style={[messageInset, isFirstInCluster && a.mt_md]}>
{hasLargeGapFromPrev && <DateDivider date={message.sentAt} />}
<View
style={[
messageInset,
isFirstInCluster ? a.mt_md : {marginTop: CLUSTERED_MESSAGE_GAP},
]}>
<View style={[a.relative]}>
{showAvatar ? (
<View
@@ -402,8 +401,15 @@ let MessageItem = ({
a.absolute,
a.bottom_0,
a.z_50,
{
transform: [{translateY: hasReactions ? -24 : 0}],
hasReactions && {
transform: [
{
translateY: platform({
ios: -29,
default: -27,
}),
},
],
},
]}>
{avatar}
@@ -436,6 +442,7 @@ let MessageItem = ({
<MessageItemEmbed
embed={message.embed}
isFromSelf={isFromSelf}
isGroupChat={isGroupChat}
squaredBottomCorner={squaredBottomCorner || hasEmbedAndText}
squaredTopCorner={squaredTopCorner}
/>
@@ -444,26 +451,22 @@ let MessageItem = ({
<Animated.View
accessibilityHint={l`Double tap or long press the message to add a reaction`}
style={[
!isFromSelf && a.ml_sm,
...(isOnlyEmoji(message.text)
? []
: [
a.rounded_xl,
a.py_sm,
a.px_md,
{
marginTop: isFirstInCluster
? 0
: CLUSTERED_MESSAGE_GAP,
backgroundColor: isFromSelf
? isPending
? pendingColor
: t.palette.primary_500
: t.palette.contrast_50,
},
isFromSelf ? a.self_end : a.self_start,
borderRadiusStyle,
]),
!isFromSelf && isGroupChat && a.ml_sm,
!isOnlyEmoji(message.text) && [
a.rounded_xl,
a.py_sm,
a.px_md,
{
marginTop: hasEmbedAndText ? CLUSTERED_MESSAGE_GAP : 0,
backgroundColor: isFromSelf
? isPending
? pendingColor
: t.palette.primary_500
: t.palette.contrast_50,
},
isFromSelf ? a.self_end : a.self_start,
borderRadiusStyle,
],
]}>
<RichText
value={rt}
+7 -6
View File
@@ -6,18 +6,19 @@ import {atoms as a, native, useTheme, web} from '#/alf'
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
import {MessageContextProvider} from './MessageContext'
const CLUSTERED_MESSAGE_GAP = 2
const BORDER_RADIUS = 20
const SQUARED_BORDER_RADIUS = 4
let MessageItemEmbed = ({
embed,
isFromSelf,
isGroupChat,
squaredTopCorner,
squaredBottomCorner,
}: {
embed: $Typed<AppBskyEmbedRecord.View>
isFromSelf: boolean
isGroupChat: boolean
squaredTopCorner: boolean
squaredBottomCorner: boolean
}): React.ReactNode => {
@@ -28,7 +29,7 @@ let MessageItemEmbed = ({
<MessageContextProvider>
<View
style={[
!isFromSelf && a.ml_sm,
!isFromSelf && isGroupChat && a.ml_sm,
native({
flexBasis: 0,
width: Math.min(screen.width, 600) / 1.4,
@@ -38,11 +39,11 @@ let MessageItemEmbed = ({
minWidth: 280,
maxWidth: 360,
}),
{
marginTop: CLUSTERED_MESSAGE_GAP,
},
// Cancel out the embed's internal a.mt_sm so the container's
// CLUSTERED_MESSAGE_GAP (2px) is the only spacing applied
{marginTop: -a.mt_sm.marginTop},
]}>
<View style={{marginTop: -8}}>
<View>
<Embed
embed={embed}
allowNestedQuotes
@@ -1,41 +1,38 @@
import {useCallback, useMemo} from 'react'
import {View} from 'react-native'
import {type ModerationDecision} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useProfileBlockMutationQueue} from '#/state/queries/profile'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft'
import {
PersonCheck_Stroke2_Corner0_Rounded as PersonCheckIcon,
PersonX_Stroke2_Corner0_Rounded as PersonXIcon,
} from '#/components/icons/Person'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
export function MessagesListBlockedFooter({
recipient: initialRecipient,
convoId,
hasMessages,
moderation,
}: {
recipient: bsky.profile.AnyProfileView
convoId: string
hasMessages: boolean
moderation: ModerationDecision
}) {
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {_} = useLingui()
const {t: l} = useLingui()
const recipient = useProfileShadow(initialRecipient)
const [__, queueUnblock] = useProfileBlockMutationQueue(recipient)
const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(recipient)
const leaveConvoControl = useDialogControl()
const reportControl = useDialogControl()
const blockedByListControl = useDialogControl()
const {listBlocks, userBlock} = useMemo(() => {
@@ -55,86 +52,78 @@ export function MessagesListBlockedFooter({
if (listBlocks.length) {
blockedByListControl.open()
} else {
queueUnblock()
void queueUnblock()
}
}, [blockedByListControl, listBlocks, queueUnblock])
return (
<View style={[hasMessages && a.pt_md, a.pb_xl, a.gap_lg]}>
<Divider />
<Text style={[a.text_md, a.font_semi_bold, a.text_center]}>
<View style={[a.p_md]}>
<View
style={[
a.align_center,
a.justify_center,
a.p_lg,
t.atoms.bg_contrast_50,
{
borderRadius: 40,
},
]}>
<PersonXIcon fill={t.atoms.text.color} size="lg" style={[a.mb_xs]} />
<Text
style={[
a.mb_xs,
a.text_center,
a.text_md,
a.font_semi_bold,
t.atoms.text,
]}>
{isBlocking
? l`You are blocking this person`
: l`This person is blocking you`}
</Text>
<Text
style={[
a.text_center,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_high,
]}>
<Trans>You can read chat history but cant send new messages.</Trans>
</Text>
{isBlocking ? (
<Trans>You have blocked this user</Trans>
) : (
<Trans>This user has blocked you</Trans>
)}
</Text>
<View style={[a.flex_row, a.justify_between, a.gap_lg, a.px_md]}>
<Button
label={l`Unblock`}
color="secondary_inverted"
size="large"
style={[a.mt_lg, a.w_full]}
onPress={onUnblockPress}>
<ButtonIcon icon={PersonCheckIcon} />
<ButtonText>
<Trans>Unblock</Trans>
</ButtonText>
</Button>
) : null}
<Button
label={_(msg`Leave chat`)}
color="secondary"
variant="solid"
size="small"
style={[a.flex_1]}
label={l`Leave chat`}
color="secondary_inverted"
size="large"
style={[a.mt_lg, a.w_full]}
onPress={leaveConvoControl.open}>
<ButtonText style={{color: t.palette.negative_500}}>
<ButtonIcon icon={LeaveIcon} />
<ButtonText>
<Trans>Leave chat</Trans>
</ButtonText>
</Button>
<Button
label={_(msg`Report`)}
color="secondary"
variant="solid"
size="small"
style={[a.flex_1]}
onPress={reportControl.open}>
<ButtonText style={{color: t.palette.negative_500}}>
<Trans>Report</Trans>
</ButtonText>
</Button>
{isBlocking && gtMobile && (
<Button
label={_(msg`Unblock`)}
color="secondary"
variant="solid"
size="small"
style={[a.flex_1]}
onPress={onUnblockPress}>
<ButtonText style={{color: t.palette.primary_500}}>
<Trans>Unblock</Trans>
</ButtonText>
</Button>
)}
<LeaveConvoPrompt
control={leaveConvoControl}
currentScreen="conversation"
convoId={convoId}
/>
<BlockedByListDialog
control={blockedByListControl}
listBlocks={listBlocks}
/>
</View>
{isBlocking && !gtMobile && (
<View style={[a.flex_row, a.justify_center, a.px_md]}>
<Button
label={_(msg`Unblock`)}
color="secondary"
variant="solid"
size="small"
style={[a.flex_1]}
onPress={onUnblockPress}>
<ButtonText style={{color: t.palette.primary_500}}>
<Trans>Unblock</Trans>
</ButtonText>
</Button>
</View>
)}
<LeaveConvoPrompt
control={leaveConvoControl}
currentScreen="conversation"
convoId={convoId}
/>
<ReportConversationPrompt control={reportControl} />
<BlockedByListDialog
control={blockedByListControl}
listBlocks={listBlocks}
/>
</View>
)
}
+62 -35
View File
@@ -6,18 +6,17 @@ import {
type ModerationOpts,
} from '@atproto/api'
import {useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context'
import {atoms as a, useTheme} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
import {Button, ButtonIcon} from '#/components/Button'
import {ButtonIcon} from '#/components/Button'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {Bell2Off_Filled_Corner0_Rounded as BellOffIcon} from '#/components/icons/Bell2'
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
@@ -33,13 +32,16 @@ const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE
export function MessagesListHeader({convo}: {convo?: ConvoWithDetails | null}) {
const t = useTheme()
const moderationOpts = useModerationOpts()
const {isWithinSplitView} = useIsWithinSplitView()
return (
<Layout.Header.Outer noBottomBorder={IS_LIQUID_GLASS}>
<View style={[a.w_full, a.flex_row, a.gap_xs, a.align_start]}>
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
<Layout.Header.BackButton />
</View>
{!isWithinSplitView && (
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
<Layout.Header.BackButton />
</View>
)}
{convo && moderationOpts ? (
convo.kind === 'direct' ? (
<ProfileHeaderReady convo={convo} moderationOpts={moderationOpts} />
@@ -116,7 +118,7 @@ function ProfileHeaderReady({
heading={
<Link
label={l`View ${displayName}s profile`}
style={[a.flex_row, a.gap_md, a.flex_1, a.pr_md]}
style={[a.flex_row, a.gap_md, a.flex_1]}
to={makeProfileLink(profile)}>
<PreviewableUserAvatar
size={PFP_SIZE}
@@ -125,14 +127,16 @@ function ProfileHeaderReady({
disableHoverCard={moderation.blocked}
/>
<View style={[a.flex_row, a.align_center, a.flex_1]}>
<Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}>
<Text
style={[a.text_md, a.font_semi_bold, a.flex_shrink]}
numberOfLines={1}>
{displayName}
</Text>
<ProfileBadges profile={profile} size="md" style={[a.pl_xs]} />
<MuteStatus muted={convo.view.muted} />
</View>
</Link>
}
muted={convo.view.muted}
settings={
<ConvoMenu
convo={convo.view}
@@ -153,39 +157,56 @@ function GroupHeaderReady({
}) {
const {t: l} = useLingui()
const navigation = useNavigation<NavigationProp>()
const handleNavigateToSettings = () => {
navigation.navigate('MessagesConversationSettings', {
conversation: convo.view.id,
})
}
const lockStatus = convo.details.lockStatus
const disabled = convo.details.lockStatus === 'locked-permanently'
return (
<Wrapper
heading={
<>
<Link
label={convo.details.name}
accessibilityHint={l`Open group chat settings`}
style={[a.flex_row, a.gap_md, a.flex_1, a.justify_start]}
to={
disabled
? '#'
: {
screen: 'MessagesConversationSettings',
params: {
conversation: convo.view.id,
},
}
}>
<AvatarBubbles size={40} profiles={convo.members} />
<Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}>
{convo.details.name}
</Text>
</>
<View style={[a.flex_row, a.flex_1, a.align_center]}>
<Text
style={[a.text_md, a.font_semi_bold, a.flex_shrink]}
numberOfLines={1}>
{convo.details.name}
</Text>
<MuteStatus muted={convo.view.muted} />
</View>
</Link>
}
muted={convo.view.muted}
settings={
<Button
<Link
to={
disabled
? '#'
: {
screen: 'MessagesConversationSettings',
params: {
conversation: convo.view.id,
},
}
}
label={l`Open group chat settings`}
disabled={lockStatus === 'locked-permanently'}
size="small"
color="secondary"
shape="round"
variant="ghost"
style={[a.bg_transparent]}
onPress={handleNavigateToSettings}>
style={[a.bg_transparent, a.justify_center]}>
<ButtonIcon icon={DotsHorizontalIcon} size="md" />
</Button>
</Link>
}
/>
)
@@ -193,22 +214,28 @@ function GroupHeaderReady({
function Wrapper({
heading,
muted,
settings,
}: {
heading: React.ReactNode
muted: boolean
settings: React.ReactNode
}) {
return (
<View style={[a.flex_1]}>
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
<View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]}>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_sm,
]}>
<View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}>
{heading}
<MuteStatus muted={muted} />
</View>
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
<View
style={[{minHeight: PFP_SIZE}, a.justify_center, a.flex_shrink_0]}>
<Layout.Header.Slot>{settings}</Layout.Header.Slot>
</View>
</View>
@@ -5,7 +5,7 @@ import {Trans} from '@lingui/react/macro'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {atoms as a, useTheme} from '#/alf'
import {canBeMessaged} from '#/components/dms/util'
import {canBeAddedToGroup} from '#/components/dms/util'
import * as Toggle from '#/components/forms/Toggle'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
@@ -19,7 +19,7 @@ export function GroupChatProfileCard({
moderationOpts: ModerationOpts
}) {
const t = useTheme()
const enabled = canBeMessaged(profile)
const enabled = canBeAddedToGroup(profile)
const moderation = moderateProfile(profile, moderationOpts)
const handle = sanitizeHandle(profile.handle, '@')
const displayName = sanitizeDisplayName(
@@ -53,7 +53,7 @@ export function GroupChatProfileCard({
<Text
style={[a.leading_snug, t.atoms.text_contrast_high]}
numberOfLines={2}>
<Trans>{handle} cant be messaged</Trans>
<Trans>{handle} cant be added</Trans>
</Text>
)}
</View>
+29 -11
View File
@@ -46,14 +46,19 @@ function getProfileAction(
export function getSystemMessageInfo(
data: ChatBskyConvoDefs.SystemMessageView['data'],
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>,
opts = {short: false},
): SystemMessageInfo | null {
if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) {
const action = getProfileAction(data.member, relatedProfiles)
return {
Icon: JoinIcon,
message: action
? msg`${action.displayName} was added to the group`
: msg`Someone was added to the group`,
? opts.short
? msg`${action.displayName} was added`
: msg`${action.displayName} was added to the group`
: opts.short
? msg`Someone was added`
: msg`Someone was added to the group`,
action: action ?? undefined,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataRemoveMember(data)) {
@@ -61,8 +66,12 @@ export function getSystemMessageInfo(
return {
Icon: LeaveIcon,
message: action
? msg`${action.displayName} was removed from the group`
: msg`Someone was removed from the group`,
? opts.short
? msg`${action.displayName} was removed`
: msg`${action.displayName} was removed from the group`
: opts.short
? msg`Someone was removed`
: msg`Someone was removed from the group`,
action: action ?? undefined,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataMemberJoin(data)) {
@@ -70,8 +79,12 @@ export function getSystemMessageInfo(
return {
Icon: JoinIcon,
message: action
? msg`${action.displayName} joined the group`
: msg`Someone joined the group`,
? opts.short
? msg`${action.displayName} joined`
: msg`${action.displayName} joined the group`
: opts.short
? msg`Someone joined`
: msg`Someone joined the group`,
action: action ?? undefined,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataMemberLeave(data)) {
@@ -79,8 +92,12 @@ export function getSystemMessageInfo(
return {
Icon: LeaveIcon,
message: action
? msg`${action.displayName} left the group`
: msg`Someone left the group`,
? opts.short
? msg`${action.displayName} left`
: msg`${action.displayName} left the group`
: opts.short
? msg`Someone left`
: msg`Someone left the group`,
action: action ?? undefined,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataLockConvo(data)) {
@@ -92,9 +109,10 @@ export function getSystemMessageInfo(
} else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) {
return {
Icon: PencilIcon,
message: data.newName
? msg`Chat title changed to ${data.newName}`
: msg`Chat title changed`,
message:
data.newName && !opts.short
? msg`Chat title changed to ${data.newName}`
: msg`Chat title changed`,
}
} else if (ChatBskyConvoDefs.isSystemMessageDataCreateJoinLink(data)) {
return {
+14
View File
@@ -24,6 +24,20 @@ export function canBeMessaged(profile: bsky.profile.AnyProfileView) {
}
}
export function canBeAddedToGroup(profile: bsky.profile.AnyProfileView) {
switch (profile.associated?.chat?.allowGroupInvites) {
case 'none':
return false
case 'all':
return true
case 'following':
case undefined:
return Boolean(profile.viewer?.followedBy)
default:
return false
}
}
export function localDateString(date: Date) {
// can't use toISOString because it should be in local time
const mm = date.getMonth()
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from '../TEMPLATE'
export const Leaflet = createSinglePathSVG({
path: 'M16.091 3.001c.385-.01 1.07.04 1.51.646q.122.17.186.334c.317-.029.759-.066 1.17-.032.395.034 1.195.163 1.661.9.411.65.308 1.316.107 1.768a3.3 3.3 0 0 1-.416.667c.142.124.302.293.428.507.302.514.316 1.083.182 1.62-.236.944-1.025 1.356-1.504 1.54-.493.19-1.07.294-1.502.371l-.05.009-.1.017c.106.368.084.759-.039 1.13-.34 1.025-1.114 1.513-1.85 1.718-.632.176-1.337.176-1.839.175h-.18c.126.508-.018 1.009-.158 1.348-.52 1.25-1.696 1.717-2.647 1.915s-2.052.204-2.966.21h-.063c-.5.002-.945.005-1.334.027-.345.666-.63 1.407-.962 2.271v.003l-.27.701c-.37.943-1.413 1.4-2.33 1.02-.918-.378-1.364-1.45-.996-2.392.473-1.207.962-2.049 1.615-3.342.175-.348.322-.628.452-.889 0-.786.057-1.341.17-2.045.19-1.183.35-1.639.585-2.718.215-.746.532-1.747 1.03-2.299.285-.317.727-.78 1.392-.812.404-.02.656.315.869.457.618-.86 1.143-2.005 1.82-2.894.542-.713 1.125-1.336 2.067-1.188.485.077 1.176.452 1.176 1.188.18-.205.729-.995.96-1.188.44-.367 1.048-.721 1.826-.743m-.347.953c-.828.023-1.412.747-1.902 1.354-.355.44-.66.82-.975.828-.308.008-.285-.204-.256-.473.042-.383.095-.88-.78-1.02-.726-.114-1.492 1.266-2.157 2.464-.455.82-.864 1.555-1.18 1.667-.482.172-.705-.03-.896-.202-.23-.208-.413-.373-.947.22-.563.623-.947 2.328-1.319 4.186-.047.237.283.39.434.203.856-1.057 2.248-2.51 3.908-3.585 1.526-.988 2.998-1.97 4.664-2.464.245-.073.315.21.08.312-1.878.82-3.491 1.903-5.181 3.348-1.9 1.589-3.696 3.703-4.607 5.427a22 22 0 0 0-1.511 3.127.6.6 0 0 0 .32.771.573.573 0 0 0 .751-.329q.134-.345.262-.68c.39-1.02.756-1.976 1.271-2.845a.21.21 0 0 1 .154-.101c.56-.069 1.227-.073 1.917-.077 1.881-.01 3.936-.023 4.487-1.348.287-.692-.06-.792-.373-.881-.255-.074-.489-.141-.335-.52.268-.655 1.138-.655 2.061-.655 1.075 0 2.22 0 2.563-1.036.147-.443-.115-.618-.347-.772-.194-.129-.366-.244-.262-.492.167-.394.89-.525 1.658-.663.917-.165 1.895-.34 2.057-.99.154-.614-.133-.812-.429-1.017-.18-.126-.364-.255-.454-.481-.11-.279.13-.576.38-.885.296-.365.604-.746.362-1.13-.276-.435-1.096-.358-1.775-.295-.424.04-.793.073-.94-.025-.148-.1-.142-.268-.136-.436.009-.262.017-.521-.566-.505',
})
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from '../TEMPLATE'
export const Offprint = createSinglePathSVG({
path: 'M7.638 11.873c.096.796.45 2.419 1.255 3.787.097.191.354.16.419 0l4.636-8.401a.394.394 0 0 0-.097-.51 3.73 3.73 0 0 0-2.575-.89c-3.703.095-3.735 4.71-3.638 6.014m5.086 6.237c3.606-.095 3.735-4.677 3.606-5.95-.064-.828-.418-2.483-1.255-3.915-.065-.127-.29-.127-.355 0l-4.668 8.465c-.064.191-.032.382.097.51a3.73 3.73 0 0 0 2.575.89m3.928-13.365-.193.35c-.129.223-.065.477.129.573C19.163 7.29 20 10.09 20 11.937c0 2.291-1.931 7.637-7.823 7.637-1.159 0-2.157-.159-3.058-.445a.415.415 0 0 0-.483.159l-.258.509c-.096.19-.322.255-.515.16l-.354-.192a.37.37 0 0 1-.16-.509l.224-.414c.097-.19.033-.413-.128-.509C4.869 16.678 4 13.846 4 12.064c0-2.291 1.77-7.637 7.694-7.637 1.224 0 2.254.159 3.155.445a.46.46 0 0 0 .515-.19l.29-.51a.366.366 0 0 1 .483-.127l.386.19c.161.096.226.319.129.51',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from '../TEMPLATE'
export const PcktFull = createSinglePathSVG({
path: 'M4.686 8.752c1.117 0 1.88.517 2.246 1.52q.247.678.249 1.643l-.001.097q-.014.94-.267 1.6l-.074.176c-.394.858-1.123 1.303-2.153 1.303a2.32 2.32 0 0 1-1.408-.455l-.035-.028v1.966a1 1 0 0 1-.008.074c-.029.178-.146.267-.359.267H.854q-.273 0-.335-.216l-.01-.035a1 1 0 0 1-.009-.09v-7.34a.7.7 0 0 1 .02-.149.27.27 0 0 1 .155-.18l.027-.01.005-.002a.5.5 0 0 1 .126-.02h2.066q.04 0 .076.007h.007l.048.013a.27.27 0 0 1 .204.231l.005.028a1 1 0 0 1 .004.07 2 2 0 0 1 .185-.131l.039-.024a2 2 0 0 1 .289-.144q.413-.17.93-.171m5.367.041c.46 0 .958.048 1.655.289l.015.005.021.008.06.021.02.007.018.008.017.007.033.014.024.012.015.008.02.01.003.004.01.005.006.004.01.007.003.003.01.008.005.003.009.008.003.003.009.007.011.012.008.009.004.004.006.008.005.005.005.007.013.023.005.009.004.007.002.003a.4.4 0 0 1 .036.134l.004.05.083 1.389a1 1 0 0 1 0 .083v.01l-.004.03-.007.043-.002.008q0 .006-.004.012v.005l-.005.014-.005.014-.002.003-.005.011-.002.004-.006.01-.002.004-.005.008q0 .003-.003.005l-.003.003-.006.009-.003.004-.018.016-.023.018-.005.003-.01.005h-.002l-.01.006q-.004 0-.008.003l-.007.002-.007.002-.009.003-.007.001-.01.003-.005.001-.013.002H12l-.012.002h-.007l-.013.001h-.048l-.013-.003-.057-.008-.124-.02a9 9 0 0 0-1.272-.112c-.804 0-1.146.265-1.146.857 0 .495.212.857 1.134.857a10 10 0 0 0 1.396-.12c.271-.049.366.06.354.326l-.083 1.388a1 1 0 0 1-.01.093l-.001.008-.003.013-.002.006-.004.012-.018.047-.007.016-.002.002-.006.011-.003.006c-.068.107-.192.153-.358.21a4.5 4.5 0 0 1-1.655.3c-1.24 0-2.135-.377-2.706-.996.306-.61.445-1.365.445-2.219 0-.828-.13-1.562-.418-2.16.574-.6 1.464-.963 2.69-.963m11.419-1.227q.354 0 .354.363v1.073c.386.048.796.116 1.265.21.2.041.327.242.285.446l-.307 1.477a.374.374 0 0 1-.438.293 17 17 0 0 0-.805-.142v.603c0 .724.425.82.71.821.117 0 .33-.024.52-.048.271-.036.39.072.378.338l-.082 1.46c-.012.29-.166.46-.415.52a5.5 5.5 0 0 1-1.24.145c-1.869 0-2.614-.978-2.614-2.982v-.985l-.165.008a.4.4 0 0 1-.262-.085l-.002-.002a.4.4 0 0 1-.04-.039l-.004-.003a.4.4 0 0 1-.05-.071l-.003-.005a.4.4 0 0 1-.023-.052l-.002-.005-.008-.025v-.006a.4.4 0 0 1-.01-.057l-.001-.011v-.006l-.001-.019v-.784c0-.165.07-.322.193-.43.191-.168.493-.435.759-.673.154-.139.296-.268.397-.363l.003-.003.744-.683c.201-.181.438-.278.71-.278zM14.052 7c.249 0 .368.12.368.362v2.52c.375-.199.75-.37 1.143-.512.91-.328 1.852-.47 3.017-.482l-.283.25a1.14 1.14 0 0 0-.385.858v.784q.002.214.083.394c-.294.02-.56.052-.806.096l.01.029c.436 1.35 1.099 3.357 1.099 3.357.082.254-.013.386-.273.386h-2.01a.524.524 0 0 1-.532-.386l-.77-2.338q-.142.09-.293.188v2.174q.002.362-.367.362h-1.665a.95.95 0 0 0 .23-.6l.083-1.39v-.007c.008-.165-.001-.484-.243-.718-.248-.24-.564-.225-.748-.194l-.033.005v-.355l.033.006c.197.035.521.046.77-.208.23-.234.24-.544.233-.713v-.009l-.083-1.388a.93.93 0 0 0-.317-.685 1.3 1.3 0 0 0-.382-.209l-.093-.032-.161-.054V7.362q0-.362.354-.362zM3.919 10.868c-.556 0-.74.423-.74 1.132s.184 1.132.74 1.132.738-.423.738-1.132-.182-1.132-.738-1.132',
})
export const Pckt = createSinglePathSVG({
path: 'M12.781 4c1.543 0 2.853.528 3.752 1.661q.132.167.25.347.537.358.963.891C18.62 8 19 9.54 19 11.355c0 1.444-.241 2.715-.777 3.73q.081.057.156.128c.238.224.376.504.463.752l.006.014.004.015c.112.359.188.88-.106 1.362q-.091.15-.205.26.024.154.013.322c-.042.569-.407.95-.721 1.18-.234.169-.568.355-.986.355a1.3 1.3 0 0 1-.627-.157 1.3 1.3 0 0 1-.63.157c-.4 0-.744-.186-.978-.356l-.01-.008c-.258-.193-.571-.503-.68-.952a2 2 0 0 1-.523-.278l-.01-.008c-.16-.12-.34-.284-.48-.497a5 5 0 0 1-.373-.1v1.687c0 .341-.085.773-.423 1.11-.333.331-.756.412-1.089.412H7.705c-.334 0-.752-.086-1.08-.42a1.46 1.46 0 0 1-.394-.836 1.4 1.4 0 0 1-.819-.402C5.084 18.491 5 18.064 5 17.723V5.72c0-.341.084-.767.412-1.102.328-.334.746-.42 1.08-.42h3.32c.27 0 .596.053.886.245A4.85 4.85 0 0 1 12.781 4m0 .929c-.95 0-1.747.277-2.368.772 0-.376-.194-.574-.601-.574h-3.32q-.582 0-.583.594v12.002q0 .594.583.594h3.32c.407 0 .601-.198.601-.594v-3.189c.63.487 1.41.782 2.321.792.005-.147.037-.294.083-.43.068-.215.17-.425.336-.585a.91.91 0 0 1 .742-.248 1 1 0 0 1 .209-.425c.236-.277.582-.354.906-.354.32 0 .667.078.902.356q.049.059.086.122c.579-.868.879-2.089.879-3.644 0-3.406-1.437-5.19-4.096-5.19m2.229 8.819c-.532 0-.744.259-.68.784l.037.194-.175-.092a.9.9 0 0 0-.394-.111c-.266 0-.44.167-.55.526-.166.488.018.774.541.866l.192.027-.137.158c-.376.368-.348.7.083 1.023.165.12.32.183.449.184.211 0 .395-.147.542-.433l.092-.176.092.176c.146.286.32.433.532.433.146 0 .294-.064.459-.184.431-.313.449-.655.082-1.033l-.128-.148.183-.027c.533-.092.697-.378.541-.876-.119-.34-.293-.507-.54-.507a.96.96 0 0 0-.405.11l-.183.084.027-.194q.124-.786-.66-.784m-3.49-5.349c.913 0 1.213.696 1.213 1.858s-.3 1.856-1.213 1.856c-.912 0-1.212-.694-1.212-1.856s.3-1.857 1.213-1.858',
})
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from '../TEMPLATE'
export const StandardSite = createSinglePathSVG({
path: 'M11.846 2.047a.275.275 0 0 1 .308 0c.108.073.166.791.283 2.227l.04.499h2.76a3.955 3.955 0 0 1 3.954 3.954v.682h-1.364v-.682a2.59 2.59 0 0 0-2.59-2.59H12.59c.123 1.38.245 2.183.605 2.84a4.37 4.37 0 0 0 1.688 1.708c.808.455 1.845.556 3.918.759l1.14.11c1.294.127 1.942.19 2.014.298a.27.27 0 0 1-.003.304c-.073.107-.722.16-2.018.266l-.743.06v2.79a3.954 3.954 0 0 1-3.955 3.955h-2.762l-.037.467c-.116 1.457-.174 2.185-.283 2.259a.275.275 0 0 1-.308 0c-.108-.073-.166-.802-.282-2.259l-.038-.467H8.691a3.955 3.955 0 0 1-3.955-3.954v-.682H6.1v.682a2.59 2.59 0 0 0 2.59 2.59h2.725c-.124-1.433-.247-2.26-.618-2.932a4.37 4.37 0 0 0-1.72-1.715c-.823-.452-1.878-.538-3.988-.71l-1.023-.084c-1.296-.106-1.945-.16-2.018-.266a.27.27 0 0 1-.003-.304c.072-.108.72-.171 2.015-.297l.676-.066V8.727a3.955 3.955 0 0 1 3.955-3.954h2.832l.04-.5c.117-1.435.175-2.153.283-2.226m5.981 10.55c-1.414.126-2.235.252-2.903.619a4.37 4.37 0 0 0-1.72 1.715c-.372.672-.495 1.499-.62 2.933h2.652a2.59 2.59 0 0 0 2.591-2.591zm-9.136-6.46A2.59 2.59 0 0 0 6.1 8.726v2.627c1.492-.153 2.336-.285 3.017-.669a4.37 4.37 0 0 0 1.688-1.707c.36-.658.483-1.46.605-2.842z',
})
+1
View File
@@ -442,6 +442,7 @@ async function resolveMedia(
title: resolvedLink.title,
description: resolvedLink.description,
thumb: blob,
associatedRefs: resolvedLink.associatedRefs,
},
}
}
+4 -2
View File
@@ -39,7 +39,8 @@ type ResolvedExternalLink = {
* The AT-URI of the Atmosphere record representing this external content, if
* it exists. Example: a site.standard.document record.
*/
associatedRecord?: LinkMeta['associatedRecord']
associatedRefs?: LinkMeta['associatedRefs']
view?: LinkMeta['view']
}
type ResolvedPostRecord = {
@@ -245,7 +246,8 @@ async function resolveExternal(
title: result.title ?? '',
description: result.description ?? '',
thumb: result.image ? await imageToThumb(result.image) : undefined,
associatedRecord: result.associatedRecord,
associatedRefs: result.associatedRefs,
view: result.view,
}
}
+52 -11
View File
@@ -30,6 +30,9 @@ export type NotificationReason =
| 'quote'
| 'chat-message'
| 'chat-reaction'
| 'chat-added-to-group'
| 'chat-removed-from-group'
| 'chat-join-request-rejected'
| 'starterpack-joined'
| 'like-via-repost'
| 'repost-via-repost'
@@ -37,6 +40,8 @@ export type NotificationReason =
| 'unverified'
| 'subscribed-post'
type ChatNotificationReason = Extract<NotificationReason, `chat-${string}`>
/**
* Manually overridden type, but retains the possibility of
* `notification.request.trigger.payload` being `undefined`, as specified in
@@ -45,7 +50,7 @@ export type NotificationReason =
export type NotificationPayload =
| undefined
| {
reason: Exclude<NotificationReason, 'chat-message' | 'chat-reaction'>
reason: Exclude<NotificationReason, ChatNotificationReason>
uri: string
subject: string
recipientDid: string
@@ -62,6 +67,25 @@ export type NotificationPayload =
messageId: string
recipientDid: string
}
| {
reason:
| 'chat-added-to-group'
| 'chat-removed-from-group'
| 'chat-join-request-rejected'
convoId: string
recipientDid: string
}
export type ChatNotificationPayload = Extract<
NonNullable<NotificationPayload>,
{reason: ChatNotificationReason}
>
export function isChatNotificationPayload(
payload: NonNullable<NotificationPayload>,
): payload is ChatNotificationPayload {
return payload.reason.startsWith('chat-')
}
const DEFAULT_HANDLER_OPTIONS = {
shouldShowBanner: false,
@@ -199,10 +223,7 @@ export function useNotificationsHandler() {
const handleNotification = (payload?: NotificationPayload) => {
if (!payload) return
if (
payload.reason === 'chat-message' ||
payload.reason === 'chat-reaction'
) {
if (isChatNotificationPayload(payload)) {
logger.debug(`useNotificationsHandler: handling chat notification`, {
payload,
})
@@ -220,7 +241,13 @@ export function useNotificationsHandler() {
} else {
setShowLoggedOut(true)
}
} else {
} else if (
payload.reason === 'chat-message' ||
payload.reason === 'chat-reaction' ||
payload.reason === 'chat-added-to-group'
) {
// chat-added-to-group routes to the convo because the recipient was
// just added and now has access.
navigation.dispatch(state => {
if (state.routes[0].name === 'Messages') {
if (
@@ -253,6 +280,12 @@ export function useNotificationsHandler() {
})
}
})
} else {
// chat-removed-from-group, chat-join-request-rejected: the convo is
// no longer accessible to the recipient, so just open the list.
navigation.dispatch(
CommonActions.navigate('MessagesTab', {screen: 'Messages'}),
)
}
} else {
const url = notificationToURL(payload)
@@ -280,11 +313,16 @@ export function useNotificationsHandler() {
logger.debug('useNotificationsHandler: incoming', {e, payload})
if (
(payload.reason === 'chat-message' ||
payload.reason === 'chat-reaction') &&
isChatNotificationPayload(payload) &&
payload.recipientDid === currentAccount?.did
) {
const shouldAlert = payload.convoId !== currentConvoId
// chat-removed-from-group / chat-join-request-rejected always alert,
// even if the recipient is currently viewing the affected convo -
// they need to know they were removed/rejected.
const shouldAlert =
payload.reason === 'chat-removed-from-group' ||
payload.reason === 'chat-join-request-rejected' ||
payload.convoId !== currentConvoId
return {
shouldShowList: shouldAlert,
shouldShowBanner: shouldAlert,
@@ -352,8 +390,8 @@ export function useNotificationsHandler() {
// Whenever there's a stored payload, that means we had to switch accounts before handling the notification.
// Whenever currentAccount changes, we should try to handle it again.
if (
(storedAccountSwitchPayload?.reason === 'chat-message' ||
storedAccountSwitchPayload?.reason === 'chat-reaction') &&
storedAccountSwitchPayload &&
isChatNotificationPayload(storedAccountSwitchPayload) &&
currentAccount?.did === storedAccountSwitchPayload.recipientDid
) {
handleNotification(storedAccountSwitchPayload)
@@ -442,6 +480,9 @@ export function notificationToURL(payload: NotificationPayload): string | null {
}
case 'chat-message':
case 'chat-reaction':
case 'chat-added-to-group':
case 'chat-removed-from-group':
case 'chat-join-request-rejected':
// should be handled separately
return null
case 'verified':
+5 -3
View File
@@ -1,4 +1,4 @@
import {type BskyAgent} from '@atproto/api'
import {type AppBskyEmbedExternal, type BskyAgent} from '@atproto/api'
import {LINK_META_PROXY} from '#/lib/constants'
import {getGiphyMetaUri} from '#/lib/strings/embed-player'
@@ -26,7 +26,8 @@ export interface LinkMeta {
* The AT-URI of the Atmosphere record representing this external content, if
* it exists. Example: a site.standard.document record.
*/
associatedRecord?: string
associatedRefs?: AppBskyEmbedExternal.External['associatedRefs']
view?: AppBskyEmbedExternal.View
}
export async function getLinkMeta(
@@ -94,7 +95,8 @@ export async function getLinkMeta(
meta.description = body.description
meta.image = body.image
meta.title = body.title
meta.associatedRecord = body.associated_record
meta.associatedRefs = body.associated_refs
meta.view = body.view || body.external_view
if (shouldFollowRedirect) {
meta.url = body.url
}
+4 -2
View File
@@ -5,21 +5,23 @@ export function niceDate(
i18n: I18n,
date: number | string | Date,
dateStyle: 'short' | 'medium' | 'long' | 'full' | 'dot separated' = 'long',
timeStyle: 'short' | 'medium' | 'long' | 'full' | 'none' = 'short',
) {
const ts = timeStyle === 'none' ? undefined : timeStyle
const d = new Date(date)
if (dateStyle === 'dot separated') {
return i18n._(
msg({
context: 'date and time formatted like this: [time] · [date]',
message: `${i18n.date(d, {timeStyle: 'short'})} · ${i18n.date(d, {dateStyle: 'medium'})}`,
message: `${i18n.date(d, {timeStyle: ts})} · ${i18n.date(d, {dateStyle: 'medium'})}`,
}),
)
}
return i18n.date(d, {
dateStyle,
timeStyle: 'short',
timeStyle: ts,
})
}
File diff suppressed because it is too large Load Diff
+1 -36
View File
@@ -31,7 +31,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useConvoQuery} from '#/state/queries/messages/conversation'
import {useSession} from '#/state/session'
import {MessagesList} from '#/screens/Messages/components/MessagesList'
import {atoms as a, useTheme, web} from '#/alf'
import {atoms as a, web} from '#/alf'
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
import * as Dialog from '#/components/Dialog'
@@ -44,7 +44,6 @@ import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
import {Error} from '#/components/Error'
import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
@@ -100,7 +99,6 @@ export function MessagesConversationScreenInner({route}: Props) {
}
function Inner({convoId}: {convoId: string}) {
const t = useTheme()
const convoState = useConvo()
const {t: l} = useLingui()
const {currentAccount} = useSession()
@@ -114,15 +112,7 @@ function Inner({convoId}: {convoId: string}) {
? parseConvoView(convoData, currentAccount?.did)
: null
// Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user,
// we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be
// empty. So, we also check for that possible state as well and render once we can.
const [hasScrolled, setHasScrolled] = useState(false)
const readyToShow =
hasScrolled ||
(isConvoActive(convoState) &&
!convoState.isFetchingHistory &&
convoState.items.length === 0)
// Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this
// state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added.
@@ -153,11 +143,6 @@ function Inner({convoId}: {convoId: string}) {
return (
<Layout.Center style={[a.flex_1]}>
{!readyToShow && (
<View style={IS_LIQUID_GLASS && {paddingTop: topInset}}>
<MessagesListHeader convo={convo} />
</View>
)}
<View style={[a.flex_1]}>
<InnerReady
convo={convo}
@@ -165,24 +150,7 @@ function Inner({convoId}: {convoId: string}) {
setHasScrolled={setHasScrolled}
isActive={isConvoActive(convoState)}
isDisabled={convoState.status === ConvoStatus.Disabled}
hasMessages={isConvoActive(convoState) && convoState.items.length > 0}
/>
{!readyToShow && (
<View
style={[
a.absolute,
a.z_10,
a.w_full,
a.h_full,
a.justify_center,
a.align_center,
t.atoms.bg,
]}>
<View style={[{marginBottom: 75}]}>
<Loader size="xl" />
</View>
</View>
)}
</View>
</Layout.Center>
)
@@ -194,14 +162,12 @@ function InnerReady({
convo,
isActive,
isDisabled,
hasMessages,
}: {
hasScrolled: boolean
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
convo: ConvoWithDetails | null
isActive: boolean
isDisabled: boolean
hasMessages: boolean
}) {
const navigation = useNavigation<NavigationProp>()
const {top: topInset} = useSafeAreaInsets()
@@ -269,7 +235,6 @@ function InnerReady({
<MessagesListBlockedFooter
recipient={primaryMember}
convoId={convo.view.id}
hasMessages={hasMessages}
moderation={primaryMemberModeration}
/>
)
@@ -27,7 +27,7 @@ import {useLockConvo} from '#/state/queries/messages/lock-conversation'
import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
import {useSession} from '#/state/session'
import {List} from '#/view/com/util/List'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
import {Button, type ButtonColor, ButtonIcon} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -78,15 +78,13 @@ type Props = NativeStackScreenProps<
>
export function MessagesConversationSettingsScreen({route}: Props) {
const {gtTablet} = useBreakpoints()
const convoId = route.params.conversation
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content align={gtTablet ? 'left' : 'platform'}>
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Group chat settings</Trans>
</Layout.Header.TitleText>

Some files were not shown because too many files have changed in this diff Show More