Merge branch 'main' into nav-client-events
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm exec lint-staged --concurrent false
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
!**/*.jsx
|
||||
!**/*.ts
|
||||
!**/*.tsx
|
||||
!**/*.mjs
|
||||
!**/*.cjs
|
||||
!*/
|
||||
|
||||
!**/*.css
|
||||
|
||||
!**/pnpm-workspace.yaml
|
||||
|
||||
# More specific ignores go below.
|
||||
.expo
|
||||
android
|
||||
|
||||
+7
-3
@@ -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
|
||||
|
||||
|
||||
@@ -262,6 +262,7 @@ module.exports = function (_config) {
|
||||
deploymentTarget: '15.1',
|
||||
buildReactNativeFromSource: true,
|
||||
ccacheEnabled: IS_DEV,
|
||||
cxxLanguageStandard: 'c++23',
|
||||
extraPods: [
|
||||
{
|
||||
name: 'MCEmojiPicker',
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -47,6 +47,9 @@ module.exports = function (api) {
|
||||
production: {
|
||||
plugins: ['transform-remove-console'],
|
||||
},
|
||||
test: {
|
||||
plugins: ['@babel/plugin-transform-class-static-block'],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Generated
+4655
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
allowBuilds:
|
||||
'@sentry/cli': true
|
||||
'core-js': true
|
||||
'esbuild': true
|
||||
'unrs-resolver': true
|
||||
minimumReleaseAgeExclude:
|
||||
- '@atproto/*'
|
||||
- '@bsky.app/*'
|
||||
@@ -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()}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 3.8.1",
|
||||
"version": ">=18",
|
||||
"promptToConfigurePushNotifications": false,
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
@@ -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',
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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
@@ -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__",
|
||||
|
||||
Generated
+424
-162
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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,5 +1,4 @@
|
||||
import '#/logger/sentry/setup'
|
||||
import '#/view/icons'
|
||||
|
||||
import {Fragment, useEffect, useState} from 'react'
|
||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && {
|
||||
|
||||
@@ -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})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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=""
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -98,6 +98,7 @@ export function Outer({
|
||||
}: React.PropsWithChildren<{
|
||||
showCancel?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
onCloseAutoFocus?: (event: Event) => void
|
||||
}>) {
|
||||
const context = useMenuContext()
|
||||
const {_} = useLingui()
|
||||
|
||||
@@ -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 can’t 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.`)
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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} can’t be messaged</Trans>
|
||||
<Trans>{handle} can’t be added</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 can’t 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} can’t be messaged</Trans>
|
||||
<Trans>{handle} can’t be added</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
@@ -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',
|
||||
})
|
||||
@@ -442,6 +442,7 @@ async function resolveMedia(
|
||||
title: resolvedLink.title,
|
||||
description: resolvedLink.description,
|
||||
thumb: blob,
|
||||
associatedRefs: resolvedLink.associatedRefs,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+527
-443
File diff suppressed because it is too large
Load Diff
@@ -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
Reference in New Issue
Block a user