Compare commits
170 Commits
lists
...
samuel/rm-code
| Author | SHA1 | Date | |
|---|---|---|---|
| d72b21b3a3 | |||
| 2077699a07 | |||
| fb3f1c3936 | |||
| 78e20c4200 | |||
| d8e7e65a9d | |||
| 16b14b9d13 | |||
| fd5b1b3a59 | |||
| c9033dbc39 | |||
| 1b0fbd503e | |||
| 0bcde99025 | |||
| 5a4e012fdc | |||
| 6ccedd5e31 | |||
| feab782ba9 | |||
| ca505fbbd2 | |||
| e5f3131644 | |||
| 54a388d132 | |||
| b1f1b2050c | |||
| 9070fa9a8d | |||
| 55e9298ee8 | |||
| c6215065bd | |||
| bd3594ceea | |||
| de6d2f1912 | |||
| faa49feec8 | |||
| 55b3474ad2 | |||
| b0eb079ff8 | |||
| cfa2534ccb | |||
| 2d626f7bbf | |||
| 5efae600dd | |||
| 339f3320c6 | |||
| 3107efb449 | |||
| faa389e060 | |||
| f15a949ef8 | |||
| bb908a5534 | |||
| a503fe27f5 | |||
| d58c6c9ebc | |||
| bd5d102826 | |||
| 32bf6cf01f | |||
| e3f5967407 | |||
| 900e569ef2 | |||
| 1883f3fc7b | |||
| 5cff45bdc8 | |||
| 0c6469f272 | |||
| b98350f927 | |||
| 206f52fb13 | |||
| 78c8a6037f | |||
| 28e132ae03 | |||
| 25a2a2c7f6 | |||
| 2a4b825637 | |||
| c00b3d1050 | |||
| 74cb81b65f | |||
| e28aa7d1ab | |||
| d4d5d6d8b9 | |||
| de68915bc0 | |||
| ea00663e7f | |||
| 12e57378e8 | |||
| 24606dbcfc | |||
| 6f31121e51 | |||
| 1abc96314b | |||
| 7cc897d096 | |||
| 6ed30213c8 | |||
| d7e8429753 | |||
| 6086bc5d5c | |||
| 199821d670 | |||
| 11dff765db | |||
| 73639aa571 | |||
| f3b6d85913 | |||
| 239d3cc535 | |||
| d49d43f6eb | |||
| a482dae936 | |||
| 25d44d8939 | |||
| e7801a0b76 | |||
| 7861cd153d | |||
| be53a168ba | |||
| b56d0c419b | |||
| 03adb4260f | |||
| 27926f093c | |||
| fe5a623b44 | |||
| fc944841ce | |||
| 66ca3e34a4 | |||
| 4788106f32 | |||
| 6414bda252 | |||
| 591fd262b3 | |||
| ede10368d5 | |||
| 1127e50248 | |||
| e4485da4ff | |||
| 6c7fd96528 | |||
| 6a1c0ff346 | |||
| ad18439148 | |||
| 7280e44ea2 | |||
| 7e6b23bd86 | |||
| d2bc22e9be | |||
| 0b50e9f109 | |||
| 0f6a99c8a5 | |||
| ea9986f127 | |||
| 5222a6e1c3 | |||
| b7f505ab00 | |||
| 34f2cc2322 | |||
| 8a30131686 | |||
| 69421d1f33 | |||
| 995808e816 | |||
| 508fa2aced | |||
| d528ae791f | |||
| b15903e0cb | |||
| 9bea450803 | |||
| fb5d56422a | |||
| ad38d905e4 | |||
| 2b34721876 | |||
| c928d6b14a | |||
| 0dd71fb087 | |||
| 54b8eacba1 | |||
| b7b47d30bf | |||
| 3561fc8112 | |||
| fe31cf4508 | |||
| a9efd94983 | |||
| 6cd6a3447d | |||
| ec8c36e614 | |||
| 84cae44cd2 | |||
| 5bf1141b55 | |||
| e1ee85622d | |||
| b1dc5179b6 | |||
| 96d77841e0 | |||
| b38edc52b8 | |||
| d68fc78e93 | |||
| 028ed2f8e2 | |||
| 67049b98c9 | |||
| 07bcf7460a | |||
| 54f70a576a | |||
| 4a9185b0f5 | |||
| 1354bf987d | |||
| 7f4e302b85 | |||
| 5e36992fba | |||
| 42a427cfd6 | |||
| 0c0c21a0aa | |||
| f170313f70 | |||
| c8d0662690 | |||
| cf4cce2225 | |||
| 0e9e705d99 | |||
| 12e09a9bd1 | |||
| 885356256e | |||
| b68f80050c | |||
| 1354fd2661 | |||
| 77bf49f212 | |||
| 02b189a40e | |||
| 5fd52b3d30 | |||
| 1e6a44f2e8 | |||
| 144d61ef76 | |||
| 381feacd26 | |||
| 5df6036c2c | |||
| 3e55e52858 | |||
| f82a6188e6 | |||
| c7ce827bfe | |||
| 9e0f192546 | |||
| d3dbb94689 | |||
| 031fa95715 | |||
| ae2c9a832f | |||
| 02a25d2aa2 | |||
| 2b32fff1d0 | |||
| 22bb9c599f | |||
| 7f4e3091f7 | |||
| 6114b33ce2 | |||
| 305ea8bdfb | |||
| a322ae412a | |||
| caa7736035 | |||
| 655964ec0b | |||
| 66526a8e10 | |||
| 8cded435d1 | |||
| bd17f04810 | |||
| bdc58045cf | |||
| 0482e917b6 | |||
| 0bc2355226 |
@@ -37,6 +37,7 @@ module.exports = {
|
||||
'Toast.Action',
|
||||
'AgeAssuranceAdmonition',
|
||||
'Span',
|
||||
'StackedButton',
|
||||
],
|
||||
impliedTextProps: [],
|
||||
suggestedTextWrappers: {
|
||||
@@ -88,7 +89,7 @@ module.exports = {
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{argsIgnorePattern: '^_', varsIgnorePattern: '^_'},
|
||||
{argsIgnorePattern: '^_', varsIgnorePattern: '^_.+'},
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'warn',
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
name: build-and-push-link-aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'bskylink/**'
|
||||
- 'Dockerfile.bskylink'
|
||||
- '.github/workflows/build-and-push-link-aws.yaml'
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
@@ -45,7 +50,7 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: true
|
||||
file: ./Dockerfile.bskylink
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -50,8 +50,8 @@ jobs:
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
id: timestamp
|
||||
uses: nanzm/get-time-action@master
|
||||
with:
|
||||
format: 'MM-DD-HH-mm-ss'
|
||||
format: "MM-DD-HH-mm-ss"
|
||||
|
||||
- name: 🚀 Upload Production Artifact
|
||||
id: upload-artifact-production
|
||||
@@ -146,6 +146,11 @@ jobs:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: 🧹 Clear Metro cache
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
# https://github.com/expo/eas-cli/issues/2959#issuecomment-2749791326
|
||||
run: rm -rf ${TMPDIR:-/tmp}/metro-cache
|
||||
|
||||
- name: 🏗️ Build Production APK
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
run: >
|
||||
@@ -182,11 +187,11 @@ jobs:
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
if: ${{ inputs.profile == 'testflight-android' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
if: ${{ inputs.profile == 'testflight-android' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
build:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "16.4"
|
||||
xcode-version: "26.0"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
@@ -98,10 +98,29 @@ jobs:
|
||||
yarn use-build-number-with-bump
|
||||
eas build -p ios
|
||||
--profile ${{ inputs.profile || 'testflight' }}
|
||||
--local --output build.ipa --non-interactive
|
||||
--local --output build.tar.gz --non-interactive
|
||||
|
||||
- name: 📂 Extract build artifact
|
||||
run: |
|
||||
if [ -f "build.tar.gz" ]; then
|
||||
echo "Extracting build.tar.gz..."
|
||||
mkdir ios-build
|
||||
tar -xzf build.tar.gz -C ios-build
|
||||
echo "Extraction completed successfully"
|
||||
else
|
||||
echo "Archive file not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
run: eas submit -p ios --non-interactive --path ios-build/ios/build/Bluesky.ipa
|
||||
|
||||
- name: 🪲 Upload dSYM to Sentry
|
||||
run: >
|
||||
SENTRY_ORG=blueskyweb
|
||||
SENTRY_PROJECT=app
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
yarn sentry-cli debug-files upload ios-build/ios/build/Bluesky.app.dSYM.zip --include-sources
|
||||
|
||||
- name: 📚 Get version from package.json
|
||||
id: get-build-info
|
||||
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
# GitHub actions are horrible so let's just copy paste this in
|
||||
buildIfNecessaryIOS:
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
||||
cancel-in-progress: true
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "16.2"
|
||||
xcode-version: "26.0"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
|
||||
@@ -119,3 +119,8 @@ src/locale/locales/**/*.js
|
||||
|
||||
# ogcard assets
|
||||
bskyogcard/src/assets/fonts/noto-*
|
||||
|
||||
# bskyweb build output
|
||||
bskyweb/static/media/*.webp
|
||||
bskyweb/static/media/*.jpg
|
||||
bskyweb/static/media/*.png
|
||||
|
||||
@@ -70,6 +70,8 @@ Bluesky is an open social network built on the AT Protocol, a flexible technolog
|
||||
|
||||
See [./LICENSE](./LICENSE) for the full license.
|
||||
|
||||
Bluesky Social PBC has committed to a software patent non-aggression pledge. For details see [the original announcement](https://bsky.social/about/blog/10-01-2025-patent-pledge).
|
||||
|
||||
## P.S.
|
||||
|
||||
We ❤️ you and all of the ways you support us. Thank you for making Bluesky a great place!
|
||||
|
||||
@@ -3,7 +3,7 @@ appId: xyz.blueskyweb.app
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ""
|
||||
SERVER_PATH: ""
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
@@ -25,7 +25,10 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn:
|
||||
id: "passwordInput"
|
||||
- inputText: "hunter22"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
# hideKeyboard on ios is borked
|
||||
# https://docs.maestro.dev/troubleshooting/known-issues#hidekeyboard-command-is-flaky
|
||||
text: "Your account"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn:
|
||||
@@ -34,7 +37,7 @@ appId: xyz.blueskyweb.app
|
||||
- inputText: "e2e-test"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "handleAvailableCheck"
|
||||
id: "handleAvailableCheck"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- assertVisible: "Give your profile a face"
|
||||
|
||||
@@ -17,63 +17,52 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eGotoLists"
|
||||
- tapOn:
|
||||
id: "newUserListBtn"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- waitForAnimationToEnd
|
||||
- assertVisible: "Create user list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- inputText: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Create user list"
|
||||
- tapOn: "People"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
|
||||
- tapOn:
|
||||
label: "Edit display name and description via the edit curatelist modal"
|
||||
point: "90%,9%"
|
||||
id: "moreOptionsBtn"
|
||||
- tapOn: "Edit list details"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: "Edit user list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- eraseText
|
||||
- inputText: "Bad Ppl"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- eraseText
|
||||
- inputText: "They bad"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Edit user list"
|
||||
- assertVisible: Bad Ppl
|
||||
- assertVisible: They bad
|
||||
|
||||
- tapOn:
|
||||
label: "Remove description via the edit curatelist modal"
|
||||
point: "90%,9%"
|
||||
id: "moreOptionsBtn"
|
||||
- tapOn: "Edit list details"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: "Edit user list"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- eraseText
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Edit user list"
|
||||
- assertNotVisible:
|
||||
id: "listDescription"
|
||||
|
||||
- tapOn:
|
||||
label: "Delete the curatelist"
|
||||
point: "90%,9%"
|
||||
id: "moreOptionsBtn"
|
||||
- tapOn: "Delete List"
|
||||
- tapOn:
|
||||
id: "confirmBtn"
|
||||
@@ -82,18 +71,15 @@ appId: xyz.blueskyweb.app
|
||||
id: "newUserListBtn"
|
||||
- tapOn:
|
||||
id: "newUserListBtn"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: "Create user list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- inputText: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Create user list"
|
||||
- tapOn: "People"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
|
||||
@@ -19,21 +19,17 @@ appId: xyz.blueskyweb.app
|
||||
id: "moderationlistsBtn"
|
||||
- tapOn: "New list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- inputText: "Muted Users"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- inputText: "Shhh"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
|
||||
# view modlist
|
||||
- assertVisible: "Muted Users"
|
||||
- assertVisible: "Shhh"
|
||||
|
||||
# DOES NOT WORK - THE BUTTON IS NOT ACCESSIBLE
|
||||
# IGNORING FOR NOW, FIX THE COMPONENTS IN THE NEXT RELEASE
|
||||
# BECAUSE THIS IS A LEGIT A11Y PROBLEM -sfn
|
||||
- tapOn: "Subscribe to this list"
|
||||
- tapOn: "Mute accounts"
|
||||
- tapOn: "Mute list"
|
||||
|
||||
@@ -30,7 +30,14 @@ appId: xyz.blueskyweb.app
|
||||
- assertVisible: "What are your interests?"
|
||||
- tapOn:
|
||||
id: "onboardingContinue"
|
||||
- assertVisible: "You're ready to go!"
|
||||
- assertVisible: "Suggested for you"
|
||||
# mock server doesn't have suggestions api
|
||||
- tapOn: "Skip to next step"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- assertVisible: "Complete onboarding and start using your account"
|
||||
- tapOn:
|
||||
id: "onboardingFinish"
|
||||
- tapOn:
|
||||
|
||||
@@ -16,7 +16,8 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eStartOnboarding"
|
||||
- tapOn: "Select an avatar"
|
||||
- waitForAnimationToEnd
|
||||
- assertVisible: "Search your library…"
|
||||
- assertVisible: "Photos"
|
||||
- assertVisible: "Collections"
|
||||
- tapOn:
|
||||
point: "50%,22%"
|
||||
- waitForAnimationToEnd
|
||||
@@ -27,7 +28,14 @@ appId: xyz.blueskyweb.app
|
||||
- assertVisible: "What are your interests?"
|
||||
- tapOn:
|
||||
id: "onboardingContinue"
|
||||
- assertVisible: "You're ready to go!"
|
||||
- assertVisible: "Suggested for you"
|
||||
# mock server doesn't have suggestions api
|
||||
- tapOn: "Skip to next step"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- assertVisible: "Complete onboarding and start using your account"
|
||||
- tapOn:
|
||||
id: "onboardingFinish"
|
||||
- tapOn:
|
||||
|
||||
@@ -31,6 +31,8 @@ appId: xyz.blueskyweb.app
|
||||
id: "viewHeaderHomeFeedPrefsBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 1"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
@@ -69,6 +71,8 @@ appId: xyz.blueskyweb.app
|
||||
id: "profilePager-selector-1"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 2"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- launchApp:
|
||||
appId: "xyz.blueskyweb.app"
|
||||
clearState: true
|
||||
appId: "xyz.blueskyweb.app"
|
||||
clearState: true
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "http://localhost:8081"
|
||||
- waitForAnimationToEnd
|
||||
- extendedWaitUntil:
|
||||
visible: "Continue"
|
||||
- swipe:
|
||||
from: "Bluesky"
|
||||
direction: DOWN
|
||||
from: "Bluesky"
|
||||
direction: DOWN
|
||||
duration: 100
|
||||
- tapOn:
|
||||
id: e2eProxyHeaderInput
|
||||
id: e2eProxyHeaderInput
|
||||
- inputText: ${output.result}
|
||||
- pressKey: Enter
|
||||
- hideKeyboard
|
||||
|
||||
@@ -378,6 +378,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
'https://music.apple.com/us/playlist/playlistName/playlistId',
|
||||
'https://music.apple.com/us/album/albumName/albumId',
|
||||
'https://music.apple.com/us/album/albumName/albumId?i=songId',
|
||||
'https://music.apple.com/us/song/songName/songId',
|
||||
|
||||
'https://vimeo.com/videoId',
|
||||
'https://vimeo.com/videoId?autoplay=0',
|
||||
@@ -604,6 +605,11 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
playerUri:
|
||||
'https://embed.music.apple.com/us/album/albumName/albumId?i=songId',
|
||||
},
|
||||
{
|
||||
type: 'apple_music_song',
|
||||
source: 'appleMusic',
|
||||
playerUri: 'https://embed.music.apple.com/us/song/songName/songId',
|
||||
},
|
||||
|
||||
{
|
||||
type: 'vimeo_video',
|
||||
|
||||
@@ -11,7 +11,7 @@ module.exports = function (_config) {
|
||||
*
|
||||
* @see https://docs.expo.dev/build-reference/variables/#built-in-environment-variables
|
||||
*/
|
||||
const PLATFORM = process.env.EAS_BUILD_PLATFORM
|
||||
const PLATFORM = process.env.EAS_BUILD_PLATFORM ?? 'web'
|
||||
|
||||
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
|
||||
const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production'
|
||||
@@ -40,7 +40,7 @@ module.exports = function (_config) {
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
icon: './assets/app-icons/ios_icon_default_light.png',
|
||||
icon: './assets/app-icons/ios_icon_default_next.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
primaryColor: '#1083fe',
|
||||
newArchEnabled: false,
|
||||
@@ -50,6 +50,10 @@ module.exports = function (_config) {
|
||||
config: {
|
||||
usesNonExemptEncryption: false,
|
||||
},
|
||||
icon:
|
||||
PLATFORM === 'web' // web build doesn't like .icon files
|
||||
? './assets/app-icons/ios_icon_default_next.png'
|
||||
: './assets/app-icons/ios_icon_default.icon',
|
||||
infoPlist: {
|
||||
UIBackgroundModes: ['remote-notification'],
|
||||
NSCameraUsageDescription:
|
||||
@@ -112,6 +116,34 @@ module.exports = function (_config) {
|
||||
'com.apple.security.application-groups': 'group.app.bsky',
|
||||
},
|
||||
privacyManifests: {
|
||||
NSPrivacyCollectedDataTypes: [
|
||||
{
|
||||
NSPrivacyCollectedDataType: 'NSPrivacyCollectedDataTypeCrashData',
|
||||
NSPrivacyCollectedDataTypeLinked: false,
|
||||
NSPrivacyCollectedDataTypeTracking: false,
|
||||
NSPrivacyCollectedDataTypePurposes: [
|
||||
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
|
||||
],
|
||||
},
|
||||
{
|
||||
NSPrivacyCollectedDataType:
|
||||
'NSPrivacyCollectedDataTypePerformanceData',
|
||||
NSPrivacyCollectedDataTypeLinked: false,
|
||||
NSPrivacyCollectedDataTypeTracking: false,
|
||||
NSPrivacyCollectedDataTypePurposes: [
|
||||
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
|
||||
],
|
||||
},
|
||||
{
|
||||
NSPrivacyCollectedDataType:
|
||||
'NSPrivacyCollectedDataTypeOtherDiagnosticData',
|
||||
NSPrivacyCollectedDataTypeLinked: false,
|
||||
NSPrivacyCollectedDataTypeTracking: false,
|
||||
NSPrivacyCollectedDataTypePurposes: [
|
||||
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
|
||||
],
|
||||
},
|
||||
],
|
||||
NSPrivacyAccessedAPITypes: [
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
@@ -143,12 +175,11 @@ module.exports = function (_config) {
|
||||
barStyle: 'light-content',
|
||||
},
|
||||
android: {
|
||||
icon: './assets/app-icons/android_icon_default_light.png',
|
||||
icon: './assets/app-icons/android_icon_default_next.png',
|
||||
adaptiveIcon: {
|
||||
foregroundImage: './assets/icon-android-foreground.png',
|
||||
monochromeImage: './assets/icon-android-foreground.png',
|
||||
backgroundImage: './assets/icon-android-background.png',
|
||||
backgroundColor: '#1185FE',
|
||||
monochromeImage: './assets/icon-android-monochrome.png',
|
||||
backgroundColor: '#006AFF',
|
||||
},
|
||||
googleServicesFile: './google-services.json',
|
||||
package: 'xyz.blueskyweb.app',
|
||||
@@ -295,18 +326,13 @@ module.exports = function (_config) {
|
||||
* Default set
|
||||
*/
|
||||
default_light: {
|
||||
ios: './assets/app-icons/ios_icon_default_light.png',
|
||||
android: './assets/app-icons/android_icon_default_light.png',
|
||||
ios: './assets/app-icons/ios_icon_legacy_light.png',
|
||||
android: './assets/app-icons/android_icon_legacy_light.png',
|
||||
prerendered: true,
|
||||
},
|
||||
default_dark: {
|
||||
ios: './assets/app-icons/ios_icon_default_dark.png',
|
||||
android: './assets/app-icons/android_icon_default_dark.png',
|
||||
prerendered: true,
|
||||
},
|
||||
next: {
|
||||
ios: './assets/app-icons/icon_default_next.png',
|
||||
android: './assets/app-icons/icon_default_next.png',
|
||||
ios: './assets/app-icons/ios_icon_legacy_dark.png',
|
||||
android: './assets/app-icons/android_icon_legacy_dark.png',
|
||||
prerendered: true,
|
||||
},
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 467 KiB After Width: | Height: | Size: 467 KiB |
|
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 486 KiB |
|
After Width: | Height: | Size: 771 KiB |
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"fill" : {
|
||||
"automatic-gradient" : "srgb:0.00000,0.41569,1.00000,1.00000"
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"fill" : "none",
|
||||
"glass" : false,
|
||||
"image-name" : "iOS transparent.png",
|
||||
"name" : "iOS transparent"
|
||||
}
|
||||
],
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 389 KiB After Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 321 KiB |
|
Before Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 125 KiB |
@@ -0,0 +1,26 @@
|
||||
## Run / Test
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
cd bskyembed
|
||||
yarn
|
||||
```
|
||||
|
||||
Run the dev server:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
You can see the embed homepage at http://localhost:5173
|
||||
|
||||
### Testbed
|
||||
|
||||
In another terminal window, run the snippet dev script:
|
||||
|
||||
```bash
|
||||
yarn dev-snippet
|
||||
```
|
||||
|
||||
You can then see the testbed page at http://localhost:5173/test
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg width="105" height="32" viewBox="0 0 105 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.7901 9.77492C21.7947 11.2889 23.9508 14.3587 24.7425 16.0059C25.5342 14.3587 27.6903 11.2889 29.6949 9.77492C31.1413 8.68251 33.485 7.83725 33.485 10.5269C33.485 11.064 33.1789 15.0393 32.9993 15.6848C32.3752 17.9285 30.1009 18.5008 28.0778 18.1544C31.6141 18.7598 32.5136 20.7653 30.5709 22.7708C26.1996 27.2831 24.7425 20.3569 24.7425 20.3569C24.7425 20.3569 23.2854 27.2831 18.9142 22.7708C16.9714 20.7653 17.871 18.7598 21.4072 18.1544C19.3841 18.5008 17.1099 17.9285 16.4857 15.6848C16.3061 15.0393 16 11.064 16 10.5269C16 7.83725 18.3437 8.68251 19.7901 9.77492Z" fill="white"/>
|
||||
<path d="M44.3863 16.3646C45.5901 16.7932 46.2292 17.7837 46.2292 18.8924C46.2292 20.7698 44.9659 21.908 42.5434 21.908H37.5942V11.3828H42.3799C44.6835 11.3828 45.7982 12.5506 45.7982 14.1028C45.7982 15.1376 45.3226 15.8915 44.3863 16.3646ZM42.2313 13.0237H39.6006V15.7732H42.2313C43.2568 15.7732 43.8067 15.2411 43.8067 14.3541C43.8067 13.5411 43.2419 13.0237 42.2313 13.0237ZM39.6006 20.2524H42.4393C43.5689 20.2524 44.1782 19.735 44.1782 18.8037C44.1782 17.828 43.5986 17.3402 42.4393 17.3402H39.6006V20.2524Z" fill="white"/>
|
||||
<path d="M49.0111 21.908H47.1385V11.3828H49.0111V21.908Z" fill="white"/>
|
||||
<path d="M55.2202 18.5524V14.2802H57.0929V21.908H55.2797V20.7993C54.7 21.6567 53.8975 22.0854 52.872 22.0854C51.252 22.0854 50.1968 21.1098 50.1968 19.3359V14.2802H52.0694V19.0254C52.0694 19.9863 52.545 20.4741 53.5111 20.4741C54.4177 20.4741 55.2202 19.8089 55.2202 18.5524Z" fill="white"/>
|
||||
<path d="M65.6167 18.2272V18.6706H59.8799C60.0137 19.9863 60.7419 20.6367 61.8714 20.6367C62.7334 20.6367 63.3131 20.2672 63.6252 19.5428H65.4235C65.0222 21.095 63.6846 22.0854 61.8566 22.0854C60.7122 22.0854 59.7907 21.7159 59.0922 20.9915C58.3937 20.2672 58.037 19.3063 58.037 18.0941C58.037 16.8967 58.3788 15.9359 59.0773 15.1967C59.7759 14.4724 60.6825 14.1028 61.8269 14.1028C62.9861 14.1028 63.9076 14.4872 64.5912 15.2411C65.2749 15.995 65.6167 17.0002 65.6167 18.2272ZM61.812 15.5515C60.7865 15.5515 60.0731 16.1428 59.8948 17.3698H63.7441C63.5806 16.2611 62.8969 15.5515 61.812 15.5515Z" fill="white"/>
|
||||
<path d="M69.7673 22.115C67.5379 22.115 66.3638 21.2428 66.2598 19.4837H68.0878C68.1918 20.4298 68.6674 20.7846 69.797 20.7846C70.8076 20.7846 71.3129 20.4741 71.3129 19.868C71.3129 19.3211 70.9562 19.055 69.8118 18.8628L68.935 18.715C67.2555 18.4341 66.4232 17.6654 66.4232 16.4089C66.4232 14.975 67.5676 14.1028 69.6038 14.1028C71.7885 14.1028 72.9181 14.9602 72.9924 16.6898H71.2238C71.1792 15.7585 70.6293 15.4332 69.6038 15.4332C68.712 15.4332 68.2662 15.7289 68.2662 16.3202C68.2662 16.8524 68.6526 17.0889 69.5443 17.2515L70.5104 17.3993C72.3681 17.7393 73.1707 18.4193 73.1707 19.7202C73.1707 21.2576 71.9223 22.115 69.7673 22.115Z" fill="white"/>
|
||||
<path d="M81.3899 21.908H79.2497L77.0204 18.3602L75.8611 19.5132V21.908H74.0182V11.3828H75.8611V17.4437L78.9822 14.2802H81.2116L78.3134 17.1628L81.3899 21.908Z" fill="white"/>
|
||||
<path d="M86.2805 16.2019L86.9047 14.2802H88.8665L85.909 22.5289C85.5968 23.3715 85.2104 23.9776 84.72 24.3176C84.2295 24.6576 83.531 24.8202 82.6095 24.8202C82.2974 24.8202 82.0299 24.8054 81.7921 24.7759V23.3124H82.5055C83.3526 23.3124 83.7688 22.795 83.7688 22.0854C83.7688 21.7306 83.6499 21.2132 83.4121 20.548L81.1827 14.2802H83.204L83.8282 16.1872C84.289 17.6211 84.6902 19.0402 85.0469 20.4446C85.3739 19.2324 85.7901 17.8132 86.2805 16.2019Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -4,6 +4,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev-snippet": "tsc --project tsconfig.snippet.json && serve -s dist -p 3000 -n",
|
||||
"build": "tsc && vite build",
|
||||
"build-snippet": "tsc --project tsconfig.snippet.json",
|
||||
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
|
||||
@@ -21,6 +22,7 @@
|
||||
"eslint-config-preact": "^1.3.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"postcss": "^8.4.38",
|
||||
"serve": "^14.2.5",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^5.8.3",
|
||||
|
||||
@@ -3,9 +3,19 @@ interface Window {
|
||||
bluesky: {
|
||||
scan: (element?: Pick<Element, 'querySelectorAll'>) => void
|
||||
}
|
||||
BSKY_DEV_EMBED_URL?: string
|
||||
}
|
||||
|
||||
const EMBED_URL = 'https://embed.bsky.app'
|
||||
/**
|
||||
* Allow url to be overwritten during development
|
||||
*/
|
||||
const IS_DEV =
|
||||
window.location.protocol === 'file:' ||
|
||||
window.location.hostname === 'localhost'
|
||||
const EMBED_URL =
|
||||
IS_DEV && window.BSKY_DEV_EMBED_URL
|
||||
? window.BSKY_DEV_EMBED_URL
|
||||
: 'https://embed.bsky.app'
|
||||
|
||||
window.bluesky = window.bluesky || {
|
||||
scan,
|
||||
|
||||
@@ -37,7 +37,7 @@ export function Container({
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="w-full bg-white text-black hover:bg-neutral-50 dark:bg-dimmedBg dark:hover:bg-dimmedBgLighten relative transition-colors max-w-[600px] min-w-[300px] flex border dark:border-slate-600 dark:text-slate-200 rounded-xl"
|
||||
className="w-full bg-brand text-black dark:bg-brand relative transition-colors max-w-[600px] min-w-[300px] flex items-center dark:text-slate-200 rounded-[20px] cursor-pointer hover:bg-opacity-90"
|
||||
onClick={() => {
|
||||
if (ref.current && href) {
|
||||
// forwardRef requires preact/compat - let's keep it simple
|
||||
@@ -49,7 +49,9 @@ export function Container({
|
||||
}
|
||||
}}>
|
||||
{href && <Link href={href} />}
|
||||
<div className="flex-1 px-4 pt-3 pb-2.5 max-w-full">{children}</div>
|
||||
<div className="flex-1 px-[6px] pt-[6px] pb-2.5 max-w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,9 +83,9 @@ export function Embed({
|
||||
return (
|
||||
<Link
|
||||
href={`/profile/${record.author.did}/post/${getRkey(record)}`}
|
||||
className="transition-colors hover:bg-neutral-100 dark:hover:bg-slate-700 border dark:border-slate-600 rounded-xl p-2 gap-1.5 w-full flex flex-col">
|
||||
className="transition-colors hover:bg-blue-50 dark:hover:bg-slate-900 border dark:border-slate-600 rounded-xl p-2 gap-1.5 w-full flex flex-col">
|
||||
<div className="flex gap-1.5 items-center">
|
||||
<div className="w-4 h-4 rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
|
||||
<div className="w-4 h-4 rounded-full bg-neutral-300 dark:bg-slate-900 shrink-0">
|
||||
<img
|
||||
className="rounded-full"
|
||||
src={record.author.avatar}
|
||||
@@ -222,7 +222,7 @@ export function Embed({
|
||||
|
||||
function Info({children}: {children: ComponentChildren}) {
|
||||
return (
|
||||
<div className="w-full rounded-xl border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50">
|
||||
<div className="w-full rounded-xl border py-2 px-2.5 flex-row flex gap-2 hover:bg-blue-50 dark:border-slate-600 dark:hover:bg-slate-900">
|
||||
<img src={infoIcon} className="w-4 h-4 shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-textLight dark:text-textDimmed">{children}</p>
|
||||
</div>
|
||||
@@ -330,7 +330,7 @@ function ExternalEmbed({
|
||||
{content.external.thumb && (
|
||||
<img
|
||||
src={content.external.thumb}
|
||||
className="aspect-[1.91/1] object-cover"
|
||||
className="aspect-[1200/630] object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="py-3 px-4">
|
||||
@@ -435,7 +435,7 @@ function StarterPackEmbed({
|
||||
<Link
|
||||
href={starterPackHref}
|
||||
className="w-full rounded-xl overflow-hidden border dark:border-slate-600 flex flex-col items-stretch">
|
||||
<img src={imageUri} className="aspect-[1.91/1] object-cover" />
|
||||
<img src={imageUri} className="aspect-[1200/630] object-cover" />
|
||||
<div className="py-3 px-4">
|
||||
<div className="flex space-x-2 items-center">
|
||||
<img src={starterPackIcon} className="w-10 h-10" />
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
} from '@atproto/api'
|
||||
import {h} from 'preact'
|
||||
|
||||
import replyIcon from '../../assets/bubble_filled_stroke2_corner2_rounded.svg'
|
||||
import likeIcon from '../../assets/heart2_filled_stroke2_corner0_rounded.svg'
|
||||
import logo from '../../assets/logo.svg'
|
||||
import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg'
|
||||
import logo from '../../assets/logo_full_name.svg'
|
||||
import {Like as LikeIcon} from '../icons/Like'
|
||||
import {Reply as ReplyIcon} from '../icons/Reply'
|
||||
import {Repost as RepostIcon} from '../icons/Repost'
|
||||
import {CONTENT_LABELS} from '../labels'
|
||||
import * as bsky from '../types/bsky'
|
||||
import {niceDate} from '../util/nice-date'
|
||||
@@ -45,10 +45,13 @@ export function Post({thread}: Props) {
|
||||
const verification = getVerificationState({profile: post.author})
|
||||
|
||||
const href = `/profile/${post.author.did}/post/${getRkey(post)}`
|
||||
|
||||
return (
|
||||
<Container href={href}>
|
||||
<div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}>
|
||||
<div className="flex gap-2.5 items-center cursor-pointer w-full max-w-full">
|
||||
<div
|
||||
className="flex-1 flex-col flex gap-2 bg-neutral-50 dark:bg-black dark:hover:bg-slate-900 hover:bg-blue-50 rounded-[14px] p-4"
|
||||
lang={record?.langs?.[0]}>
|
||||
<div className="flex gap-2.5 items-center cursor-pointer w-full max-w-full ">
|
||||
<Link
|
||||
href={`/profile/${post.author.did}`}
|
||||
className="rounded-full shrink-0">
|
||||
@@ -80,57 +83,66 @@ export function Post({thread}: Props) {
|
||||
@{post.author.handle}
|
||||
</Link>
|
||||
</div>
|
||||
<Link
|
||||
href={href}
|
||||
className="transition-transform hover:scale-110 shrink-0 self-start">
|
||||
<img src={logo} className="h-8" />
|
||||
</Link>
|
||||
</div>
|
||||
<PostContent record={record} />
|
||||
<Embed content={post.embed} labels={post.labels} />
|
||||
<Link href={href}>
|
||||
<time
|
||||
datetime={new Date(post.indexedAt).toISOString()}
|
||||
className="text-textLight dark:text-textDimmed mt-1 text-sm hover:underline">
|
||||
{niceDate(post.indexedAt)}
|
||||
</time>
|
||||
</Link>
|
||||
<div className="border-t dark:border-slate-600 w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
|
||||
{!!post.likeCount && (
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={likeIcon} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
|
||||
{prettyNumber(post.likeCount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!!post.repostCount && (
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={repostIcon} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
|
||||
{prettyNumber(post.repostCount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={replyIcon} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
|
||||
Reply
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between w-full pt-2.5 text-sm">
|
||||
<div className="flex items-center gap-3 text-sm cursor-pointer">
|
||||
{!!post.likeCount && (
|
||||
<div className="flex items-center gap-1 cursor-pointer group">
|
||||
<LikeIcon
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"
|
||||
/>
|
||||
<p className="font-medium text-slate-600 text-neutral-600 dark:text-neutral-300 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors dark:text-slate-400">
|
||||
{prettyNumber(post.likeCount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!!post.replyCount && (
|
||||
<div className="flex items-center gap-1 cursor-pointer group">
|
||||
<ReplyIcon
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"
|
||||
/>
|
||||
<p className="font-medium text-slate-600 text-neutral-600 dark:text-neutral-300 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors dark:text-slate-400">
|
||||
{prettyNumber(post.replyCount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!!post.repostCount && (
|
||||
<div className="flex items-center gap-1 cursor-pointer group">
|
||||
<RepostIcon
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"
|
||||
/>
|
||||
<p className="font-medium text-slate-600 dark:text-slate-400 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors">
|
||||
{prettyNumber(post.repostCount)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<p className="cursor-pointer text-brand dark:text-brandLighten font-bold hover:underline hidden min-[450px]:inline">
|
||||
{post.replyCount
|
||||
? `Read ${prettyNumber(post.replyCount)} ${
|
||||
post.replyCount > 1 ? 'replies' : 'reply'
|
||||
} on Bluesky`
|
||||
: `View on Bluesky`}
|
||||
</p>
|
||||
<p className="cursor-pointer text-brand font-bold hover:underline min-[450px]:hidden">
|
||||
<span className="hidden min-[380px]:inline">View on </span>Bluesky
|
||||
</p>
|
||||
<Link href={href}>
|
||||
<time
|
||||
datetime={new Date(post.indexedAt).toISOString()}
|
||||
className="text-slate-500 dark:text-textDimmed text-sm hover:underline dark:text-slate-500">
|
||||
{niceDate(post.indexedAt)}
|
||||
</time>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end pt-2">
|
||||
<Link
|
||||
href={href}
|
||||
className="transition-transform hover:scale-110 shrink-0">
|
||||
<img src={logo} className="h-8" />
|
||||
</Link>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {h} from 'preact'
|
||||
|
||||
export const Like = ({
|
||||
width = 16,
|
||||
height = 17,
|
||||
fill = 'currentColor',
|
||||
className,
|
||||
}: {
|
||||
width?: number
|
||||
height?: number
|
||||
fill?: string
|
||||
className?: string
|
||||
}) => (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.1561 3.62664C10.3307 3.44261 9.35086 3.65762 8.47486 4.54615C8.34958 4.67323 8.17857 4.74478 8.00012 4.74478C7.82167 4.74478 7.65066 4.67324 7.52538 4.54616C6.64938 3.65762 5.66955 3.44261 4.84416 3.62664C4.0022 3.81438 3.25812 4.43047 2.89709 5.33069C2.21997 7.01907 2.83524 10.1257 8.00015 13.1315C13.165 10.1257 13.7803 7.01906 13.1032 5.33069C12.7421 4.43047 11.998 3.81437 11.1561 3.62664ZM14.3407 4.83438C15.4101 7.50098 14.0114 11.2942 8.32611 14.4808C8.12362 14.5943 7.87668 14.5943 7.6742 14.4808C1.98891 11.2942 0.590133 7.501 1.65956 4.83439C2.1788 3.53968 3.26862 2.61187 4.55399 2.32527C5.68567 2.07294 6.92237 2.32723 8.00012 3.18278C9.07786 2.32723 10.3146 2.07294 11.4462 2.32526C12.7316 2.61186 13.8214 3.53967 14.3407 4.83438Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
import {h} from 'preact'
|
||||
|
||||
export const Reply = ({
|
||||
width = 16,
|
||||
height = 17,
|
||||
fill = 'currentColor',
|
||||
className,
|
||||
}: {
|
||||
width?: number
|
||||
height?: number
|
||||
fill?: string
|
||||
className?: string
|
||||
}) => (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.3335 4.23242C1.3335 3.12785 2.22893 2.23242 3.3335 2.23242H12.6668C13.7714 2.23242 14.6668 3.12785 14.6668 4.23242V10.8991C14.6668 12.0037 13.7714 12.8991 12.6668 12.8991H8.18482L5.00983 14.8041C4.80387 14.9277 4.54737 14.9309 4.33836 14.8126C4.12936 14.6942 4.00016 14.4726 4.00016 14.2324V12.8991H3.3335C2.22893 12.8991 1.3335 12.0037 1.3335 10.8991V4.23242ZM3.3335 3.56576C2.96531 3.56576 2.66683 3.86423 2.66683 4.23242V10.8991C2.66683 11.2673 2.96531 11.5658 3.3335 11.5658H4.66683C5.03502 11.5658 5.3335 11.8642 5.3335 12.2324V13.055L7.65717 11.6608C7.76078 11.5986 7.87933 11.5658 8.00016 11.5658H12.6668C13.035 11.5658 13.3335 11.2673 13.3335 10.8991V4.23242C13.3335 3.86423 13.035 3.56576 12.6668 3.56576H3.3335Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
import {h} from 'preact'
|
||||
|
||||
export const Repost = ({
|
||||
width = 16,
|
||||
height = 17,
|
||||
fill = 'currentColor',
|
||||
className,
|
||||
}: {
|
||||
width?: number
|
||||
height?: number
|
||||
fill?: string
|
||||
className?: string
|
||||
}) => (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3.86204 9.76164C4.12239 9.50134 4.54442 9.50131 4.80475 9.76164C5.06503 10.022 5.06503 10.444 4.80475 10.7044L3.94277 11.5663H11.3334C12.0697 11.5663 12.6667 10.9693 12.6667 10.233V8.89966C12.6667 8.53147 12.9652 8.233 13.3334 8.233C13.7015 8.23305 14.0001 8.53151 14.0001 8.89966V10.233C14.0001 11.7057 12.8061 12.8996 11.3334 12.8997H3.94277L4.80475 13.7616C5.06503 14.022 5.06503 14.444 4.80475 14.7044C4.54442 14.9647 4.12239 14.9646 3.86204 14.7044L2.3334 13.1757C1.8127 12.655 1.8127 11.811 2.3334 11.2903L3.86204 9.76164ZM2.00006 7.56633V6.233C2.00006 4.76024 3.19397 3.56633 4.66673 3.56633H12.0574L11.1954 2.70435C10.935 2.444 10.935 2.02199 11.1954 1.76164C11.4557 1.50134 11.8778 1.50131 12.1381 1.76164L13.6667 3.29029C14.1873 3.81096 14.1873 4.65503 13.6667 5.17571L12.1381 6.70435C11.8778 6.96468 11.4557 6.96465 11.1954 6.70435C10.935 6.444 10.935 6.02199 11.1954 5.76164L12.0574 4.89966H4.66673C3.93035 4.89966 3.3334 5.49662 3.3334 6.233V7.56633C3.3334 7.93449 3.03487 8.23294 2.66673 8.233C2.29854 8.233 2.00006 7.93452 2.00006 7.56633Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -1,10 +1,40 @@
|
||||
import fs from 'node:fs'
|
||||
import {resolve} from 'node:path'
|
||||
|
||||
import preact from '@preact/preset-vite'
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
import type {UserConfig} from 'vite'
|
||||
import type {Plugin, UserConfig} from 'vite'
|
||||
import paths from 'vite-tsconfig-paths'
|
||||
|
||||
/**
|
||||
* World's hackiest router, for dev only. Serves `/post.html` to requests that start with `/embed/`
|
||||
*/
|
||||
function devOnlyRouter(): Plugin {
|
||||
return {
|
||||
name: 'embed-to-post-html',
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
const url = req.url || ''
|
||||
if (!url.startsWith('/embed/')) return next()
|
||||
|
||||
const html = fs.readFileSync(
|
||||
resolve(process.cwd(), 'post.html'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
server
|
||||
.transformIndexHtml(url, html)
|
||||
.then(transformed => {
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
res.end(transformed)
|
||||
})
|
||||
.catch(next)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const config: UserConfig = {
|
||||
plugins: [
|
||||
preact(),
|
||||
@@ -12,6 +42,7 @@ const config: UserConfig = {
|
||||
legacy({
|
||||
targets: ['defaults', 'not IE 11'],
|
||||
}),
|
||||
devOnlyRouter(),
|
||||
],
|
||||
build: {
|
||||
assetsDir: 'static',
|
||||
|
||||
@@ -1447,6 +1447,11 @@
|
||||
regenerator-runtime "^0.14.1"
|
||||
systemjs "^6.15.1"
|
||||
|
||||
"@zeit/schemas@2.36.0":
|
||||
version "2.36.0"
|
||||
resolved "https://registry.yarnpkg.com/@zeit/schemas/-/schemas-2.36.0.tgz#7a1b53f4091e18d0b404873ea3e3c83589c765f2"
|
||||
integrity sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==
|
||||
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
@@ -1462,6 +1467,16 @@ acorn@^8.9.0:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
|
||||
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
|
||||
|
||||
ajv@8.12.0:
|
||||
version "8.12.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
|
||||
integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^6.12.4:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
@@ -1472,6 +1487,13 @@ ajv@^6.12.4:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ansi-align@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"
|
||||
integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==
|
||||
dependencies:
|
||||
string-width "^4.1.0"
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
@@ -1507,7 +1529,12 @@ anymatch@~3.1.2:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
arg@^5.0.2:
|
||||
arch@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
|
||||
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
|
||||
|
||||
arg@5.0.2, arg@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
|
||||
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
|
||||
@@ -1684,6 +1711,20 @@ boolbase@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
|
||||
|
||||
boxen@7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.0.0.tgz#9e5f8c26e716793fc96edcf7cf754cdf5e3fbf32"
|
||||
integrity sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==
|
||||
dependencies:
|
||||
ansi-align "^3.0.1"
|
||||
camelcase "^7.0.0"
|
||||
chalk "^5.0.1"
|
||||
cli-boxes "^3.0.0"
|
||||
string-width "^5.1.2"
|
||||
type-fest "^2.13.0"
|
||||
widest-line "^4.0.1"
|
||||
wrap-ansi "^8.0.1"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
@@ -1738,6 +1779,16 @@ buffer-from@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
bytes@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
|
||||
integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==
|
||||
|
||||
bytes@3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||
|
||||
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
||||
@@ -1759,6 +1810,11 @@ camelcase-css@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
|
||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||
|
||||
camelcase@^7.0.0:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048"
|
||||
integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==
|
||||
|
||||
caniuse-lite@^1.0.30001524:
|
||||
version "1.0.30001606"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz#b4d5f67ab0746a3b8b5b6d1f06e39c51beb39a9e"
|
||||
@@ -1769,7 +1825,19 @@ caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.300017
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz#22e9706422ad37aa50556af8c10e40e2d93a8b85"
|
||||
integrity sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==
|
||||
|
||||
chalk@^4.0.0:
|
||||
chalk-template@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-0.4.0.tgz#692c034d0ed62436b9062c1707fadcd0f753204b"
|
||||
integrity sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
|
||||
chalk@5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6"
|
||||
integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==
|
||||
|
||||
chalk@^4.0.0, chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
@@ -1777,6 +1845,11 @@ chalk@^4.0.0:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^5.0.1:
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea"
|
||||
integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==
|
||||
|
||||
chokidar@^3.5.3:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
||||
@@ -1792,6 +1865,20 @@ chokidar@^3.5.3:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
cli-boxes@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145"
|
||||
integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==
|
||||
|
||||
clipboardy@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-3.0.0.tgz#f3876247404d334c9ed01b6f269c11d09a5e3092"
|
||||
integrity sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==
|
||||
dependencies:
|
||||
arch "^2.2.0"
|
||||
execa "^5.1.1"
|
||||
is-wsl "^2.2.0"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
@@ -1814,11 +1901,36 @@ commander@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
|
||||
|
||||
compressible@~2.0.18:
|
||||
version "2.0.18"
|
||||
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
|
||||
integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
|
||||
dependencies:
|
||||
mime-db ">= 1.43.0 < 2"
|
||||
|
||||
compression@1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79"
|
||||
integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
compressible "~2.0.18"
|
||||
debug "2.6.9"
|
||||
negotiator "~0.6.4"
|
||||
on-headers "~1.1.0"
|
||||
safe-buffer "5.2.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
content-disposition@0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
|
||||
integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==
|
||||
|
||||
convert-source-map@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
||||
@@ -1845,6 +1957,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
cross-spawn@^7.0.3:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
|
||||
dependencies:
|
||||
path-key "^3.1.0"
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
css-select@^5.1.0:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e"
|
||||
@@ -1893,6 +2014,13 @@ data-view-byte-offset@^1.0.0:
|
||||
es-errors "^1.3.0"
|
||||
is-data-view "^1.0.1"
|
||||
|
||||
debug@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.4.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
|
||||
@@ -1907,6 +2035,11 @@ debug@^4.3.2:
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
deep-extend@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||
|
||||
deep-is@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
@@ -2356,6 +2489,21 @@ esutils@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
execa@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
|
||||
integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.3"
|
||||
get-stream "^6.0.0"
|
||||
human-signals "^2.1.0"
|
||||
is-stream "^2.0.0"
|
||||
merge-stream "^2.0.0"
|
||||
npm-run-path "^4.0.1"
|
||||
onetime "^5.1.2"
|
||||
signal-exit "^3.0.3"
|
||||
strip-final-newline "^2.0.0"
|
||||
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
@@ -2496,6 +2644,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@
|
||||
has-symbols "^1.0.3"
|
||||
hasown "^2.0.0"
|
||||
|
||||
get-stream@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
|
||||
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
|
||||
|
||||
get-symbol-description@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5"
|
||||
@@ -2631,6 +2784,11 @@ he@1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
human-signals@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
|
||||
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
|
||||
|
||||
ignore@^5.2.0:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
|
||||
@@ -2662,6 +2820,11 @@ inherits@2:
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ini@~1.3.0:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
|
||||
internal-slot@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
|
||||
@@ -2741,6 +2904,11 @@ is-date-object@^1.0.1, is-date-object@^1.0.5:
|
||||
dependencies:
|
||||
has-tostringtag "^1.0.0"
|
||||
|
||||
is-docker@^2.0.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
|
||||
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
|
||||
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
@@ -2799,6 +2967,11 @@ is-path-inside@^3.0.3:
|
||||
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
|
||||
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
|
||||
|
||||
is-port-reachable@4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-port-reachable/-/is-port-reachable-4.0.0.tgz#dac044091ef15319c8ab2f34604d8794181f8c2d"
|
||||
integrity sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==
|
||||
|
||||
is-regex@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
|
||||
@@ -2819,6 +2992,11 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3:
|
||||
dependencies:
|
||||
call-bind "^1.0.7"
|
||||
|
||||
is-stream@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
|
||||
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
|
||||
|
||||
is-string@^1.0.5, is-string@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
|
||||
@@ -2860,6 +3038,13 @@ is-weakset@^2.0.3:
|
||||
call-bind "^1.0.7"
|
||||
get-intrinsic "^1.2.4"
|
||||
|
||||
is-wsl@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
|
||||
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
|
||||
dependencies:
|
||||
is-docker "^2.0.0"
|
||||
|
||||
isarray@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||
@@ -2932,6 +3117,11 @@ json-schema-traverse@^0.4.1:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||
|
||||
json-schema-traverse@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
@@ -3047,6 +3237,11 @@ meow@^13.0.0:
|
||||
resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f"
|
||||
integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==
|
||||
|
||||
merge-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
|
||||
|
||||
merge2@^1.3.0, merge2@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
@@ -3060,7 +3255,29 @@ micromatch@^4.0.4, micromatch@^4.0.5:
|
||||
braces "^3.0.2"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
"mime-db@>= 1.43.0 < 2":
|
||||
version "1.54.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
|
||||
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
|
||||
|
||||
mime-db@~1.33.0:
|
||||
version "1.33.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
|
||||
integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
|
||||
|
||||
mime-types@2.1.18:
|
||||
version "2.1.18"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
|
||||
integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
|
||||
dependencies:
|
||||
mime-db "~1.33.0"
|
||||
|
||||
mimic-fn@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
minimatch@3.1.2, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
@@ -3074,11 +3291,21 @@ minimatch@^9.0.1:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.0:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
|
||||
integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
@@ -3118,6 +3345,11 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
negotiator@~0.6.4:
|
||||
version "0.6.4"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
|
||||
integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
|
||||
|
||||
node-html-parser@^6.1.12:
|
||||
version "6.1.13"
|
||||
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.13.tgz#a1df799b83df5c6743fcd92740ba14682083b7e4"
|
||||
@@ -3141,6 +3373,13 @@ normalize-range@^0.1.2:
|
||||
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
||||
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
|
||||
|
||||
npm-run-path@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
||||
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
|
||||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
nth-check@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
|
||||
@@ -3215,6 +3454,11 @@ object.values@^1.1.6, object.values@^1.1.7:
|
||||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
on-headers@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65"
|
||||
integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
@@ -3222,6 +3466,13 @@ once@^1.3.0:
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
onetime@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
|
||||
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
|
||||
dependencies:
|
||||
mimic-fn "^2.1.0"
|
||||
|
||||
optionator@^0.9.3:
|
||||
version "0.9.3"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
|
||||
@@ -3265,7 +3516,12 @@ path-is-absolute@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
|
||||
|
||||
path-key@^3.1.0:
|
||||
path-is-inside@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
|
||||
integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==
|
||||
|
||||
path-key@^3.0.0, path-key@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
|
||||
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
|
||||
@@ -3283,6 +3539,11 @@ path-scurry@^1.10.2:
|
||||
lru-cache "^10.2.0"
|
||||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
|
||||
path-to-regexp@3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b"
|
||||
integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==
|
||||
|
||||
path-type@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||
@@ -3409,6 +3670,21 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
range-parser@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
|
||||
integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==
|
||||
|
||||
rc@^1.0.1, rc@^1.1.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||
dependencies:
|
||||
deep-extend "^0.6.0"
|
||||
ini "~1.3.0"
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
@@ -3480,6 +3756,21 @@ regexpu-core@^6.2.0:
|
||||
unicode-match-property-ecmascript "^2.0.0"
|
||||
unicode-match-property-value-ecmascript "^2.1.0"
|
||||
|
||||
registry-auth-token@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20"
|
||||
integrity sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==
|
||||
dependencies:
|
||||
rc "^1.1.6"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
registry-url@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
|
||||
integrity sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==
|
||||
dependencies:
|
||||
rc "^1.0.1"
|
||||
|
||||
regjsgen@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab"
|
||||
@@ -3492,6 +3783,11 @@ regjsparser@^0.12.0:
|
||||
dependencies:
|
||||
jsesc "~3.0.2"
|
||||
|
||||
require-from-string@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
@@ -3582,6 +3878,11 @@ safe-array-concat@^1.1.2:
|
||||
has-symbols "^1.0.3"
|
||||
isarray "^2.0.5"
|
||||
|
||||
safe-buffer@5.2.1, safe-buffer@^5.0.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-regex-test@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377"
|
||||
@@ -3603,6 +3904,36 @@ semver@^7.3.7, semver@^7.5.4:
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
serve-handler@6.1.6:
|
||||
version "6.1.6"
|
||||
resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1"
|
||||
integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==
|
||||
dependencies:
|
||||
bytes "3.0.0"
|
||||
content-disposition "0.5.2"
|
||||
mime-types "2.1.18"
|
||||
minimatch "3.1.2"
|
||||
path-is-inside "1.0.2"
|
||||
path-to-regexp "3.3.0"
|
||||
range-parser "1.2.0"
|
||||
|
||||
serve@^14.2.5:
|
||||
version "14.2.5"
|
||||
resolved "https://registry.yarnpkg.com/serve/-/serve-14.2.5.tgz#569e333b99a484b3a6d25acce4a569c8c4f96373"
|
||||
integrity sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==
|
||||
dependencies:
|
||||
"@zeit/schemas" "2.36.0"
|
||||
ajv "8.12.0"
|
||||
arg "5.0.2"
|
||||
boxen "7.0.0"
|
||||
chalk "5.0.1"
|
||||
chalk-template "0.4.0"
|
||||
clipboardy "3.0.0"
|
||||
compression "1.8.1"
|
||||
is-port-reachable "4.0.0"
|
||||
serve-handler "6.1.6"
|
||||
update-check "1.5.4"
|
||||
|
||||
set-function-length@^1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
|
||||
@@ -3647,6 +3978,11 @@ side-channel@^1.0.4, side-channel@^1.0.6:
|
||||
get-intrinsic "^1.2.4"
|
||||
object-inspect "^1.13.1"
|
||||
|
||||
signal-exit@^3.0.3:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
||||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
||||
|
||||
signal-exit@^4.0.1:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
|
||||
@@ -3791,11 +4127,21 @@ strip-ansi@^7.0.1:
|
||||
dependencies:
|
||||
ansi-regex "^6.0.1"
|
||||
|
||||
strip-final-newline@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
|
||||
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
|
||||
|
||||
sucrase@^3.32.0:
|
||||
version "3.35.0"
|
||||
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263"
|
||||
@@ -3937,6 +4283,11 @@ type-fest@^0.20.2:
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||
|
||||
type-fest@^2.13.0:
|
||||
version "2.19.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
|
||||
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
|
||||
|
||||
typed-array-buffer@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3"
|
||||
@@ -4034,6 +4385,14 @@ update-browserslist-db@^1.0.13, update-browserslist-db@^1.1.3:
|
||||
escalade "^3.2.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
update-check@1.5.4:
|
||||
version "1.5.4"
|
||||
resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.5.4.tgz#5b508e259558f1ad7dbc8b4b0457d4c9d28c8743"
|
||||
integrity sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==
|
||||
dependencies:
|
||||
registry-auth-token "3.3.2"
|
||||
registry-url "3.1.0"
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
|
||||
@@ -4046,6 +4405,11 @@ util-deprecate@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||
|
||||
vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||
|
||||
vite-prerender-plugin@^0.5.3:
|
||||
version "0.5.11"
|
||||
resolved "https://registry.yarnpkg.com/vite-prerender-plugin/-/vite-prerender-plugin-0.5.11.tgz#83e4f29e03269dceb763fb5ec2376dcc502aa79f"
|
||||
@@ -4138,6 +4502,13 @@ which@^2.0.1:
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
widest-line@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2"
|
||||
integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==
|
||||
dependencies:
|
||||
string-width "^5.0.1"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
@@ -4147,7 +4518,7 @@ which@^2.0.1:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
|
||||
|
||||
@@ -1,29 +1,62 @@
|
||||
import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js'
|
||||
|
||||
async function main() {
|
||||
const env = readEnv()
|
||||
const cfg = envToCfg(env)
|
||||
if (cfg.db.migrationUrl) {
|
||||
const migrateDb = Database.postgres({
|
||||
url: cfg.db.migrationUrl,
|
||||
schema: cfg.db.schema,
|
||||
try {
|
||||
httpLogger.info('Starting blink service')
|
||||
|
||||
const env = readEnv()
|
||||
const cfg = envToCfg(env)
|
||||
|
||||
httpLogger.info(
|
||||
{
|
||||
port: cfg.service.port,
|
||||
safelinkEnabled: cfg.service.safelinkEnabled,
|
||||
hasDbUrl: !!cfg.db.url,
|
||||
hasDbMigrationUrl: !!cfg.db.migrationUrl,
|
||||
},
|
||||
'Configuration loaded',
|
||||
)
|
||||
|
||||
if (cfg.db.migrationUrl) {
|
||||
httpLogger.info('Running database migrations')
|
||||
const migrateDb = Database.postgres({
|
||||
url: cfg.db.migrationUrl,
|
||||
schema: cfg.db.schema,
|
||||
})
|
||||
await migrateDb.migrateToLatestOrThrow()
|
||||
await migrateDb.close()
|
||||
httpLogger.info('Database migrations completed')
|
||||
}
|
||||
|
||||
httpLogger.info('Creating LinkService')
|
||||
const link = await LinkService.create(cfg)
|
||||
|
||||
if (link.ctx.cfg.service.safelinkEnabled) {
|
||||
httpLogger.info('Starting Safelink client')
|
||||
link.ctx.safelinkClient.runFetchEvents()
|
||||
}
|
||||
|
||||
await link.start()
|
||||
httpLogger.info('Link service is running')
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
httpLogger.info('Link service is stopping')
|
||||
await link.destroy()
|
||||
httpLogger.info('Link service is stopped')
|
||||
})
|
||||
await migrateDb.migrateToLatestOrThrow()
|
||||
await migrateDb.close()
|
||||
} catch (error) {
|
||||
httpLogger.error(
|
||||
{
|
||||
error: String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
},
|
||||
'Failed to start blink service',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const link = await LinkService.create(cfg)
|
||||
|
||||
if (link.ctx.cfg.service.safelinkEnabled) {
|
||||
link.ctx.safelinkClient.runFetchEvents()
|
||||
}
|
||||
|
||||
await link.start()
|
||||
httpLogger.info('link service is running')
|
||||
process.on('SIGTERM', async () => {
|
||||
httpLogger.info('link service is stopping')
|
||||
await link.destroy()
|
||||
httpLogger.info('link service is stopped')
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
main().catch(error => {
|
||||
console.error('Unhandled startup error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ import {redirectLogger} from '../logger.js'
|
||||
|
||||
const SAFELINK_MIN_FETCH_INTERVAL = 1_000
|
||||
const SAFELINK_MAX_FETCH_INTERVAL = 10_000
|
||||
const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+.-]*:/
|
||||
const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//
|
||||
|
||||
export class SafelinkClient {
|
||||
private domainCache: LRUCache<string, SafelinkRule | 'ok'>
|
||||
|
||||
@@ -34,6 +34,16 @@ export class Database {
|
||||
|
||||
static postgres(opts: PgOptions): Database {
|
||||
const {schema, url, txLockNonce} = opts
|
||||
log.info(
|
||||
{
|
||||
schema,
|
||||
poolSize: opts.poolSize,
|
||||
poolMaxUses: opts.poolMaxUses,
|
||||
poolIdleTimeoutMs: opts.poolIdleTimeoutMs,
|
||||
},
|
||||
'Creating database connection',
|
||||
)
|
||||
|
||||
const pool =
|
||||
opts.pool ??
|
||||
new Pg.Pool({
|
||||
|
||||
@@ -62,7 +62,10 @@ export class CardService {
|
||||
// Start main application server
|
||||
this.server = this.app.listen(this.ctx.cfg.service.port)
|
||||
this.server.keepAliveTimeout = 90000
|
||||
this.terminator = createHttpTerminator({server: this.server})
|
||||
this.terminator = createHttpTerminator({
|
||||
server: this.server,
|
||||
gracefulTerminationTimeout: 15000, // 15s timeout for in-flight requests
|
||||
})
|
||||
await events.once(this.server, 'listening')
|
||||
|
||||
// Start separate metrics server
|
||||
@@ -73,13 +76,32 @@ export class CardService {
|
||||
})
|
||||
|
||||
this.metricsServer = metricsApp.listen(this.ctx.cfg.service.metricsPort)
|
||||
this.metricsTerminator = createHttpTerminator({server: this.metricsServer})
|
||||
this.metricsTerminator = createHttpTerminator({
|
||||
server: this.metricsServer,
|
||||
gracefulTerminationTimeout: 2000, // 2s timeout for metrics server
|
||||
})
|
||||
await events.once(this.metricsServer, 'listening')
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
const startTime = Date.now()
|
||||
|
||||
this.ctx.abortController.abort()
|
||||
await this.terminator?.terminate()
|
||||
await this.metricsTerminator?.terminate()
|
||||
|
||||
const shutdownPromises = []
|
||||
|
||||
if (this.terminator) {
|
||||
shutdownPromises.push(this.terminator.terminate())
|
||||
}
|
||||
|
||||
if (this.metricsTerminator) {
|
||||
shutdownPromises.push(this.metricsTerminator.terminate())
|
||||
}
|
||||
|
||||
await Promise.all(shutdownPromises)
|
||||
|
||||
const elapsed = Date.now() - startTime
|
||||
const {httpLogger} = await import('./logger.js')
|
||||
httpLogger.info(`Graceful shutdown completed in ${elapsed}ms`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,6 +458,18 @@ func (srv *Server) WebHome(c echo.Context) error {
|
||||
return c.Render(http.StatusOK, "home.html", data)
|
||||
}
|
||||
|
||||
// Posts that include these labels will not have embeds passed to the metadata
|
||||
// template.
|
||||
var hideEmbedLabels = map[string]bool{
|
||||
"nudity": true,
|
||||
"porn": true,
|
||||
"sexual": true,
|
||||
"sexual-figurative": true,
|
||||
"graphic-media": true,
|
||||
"self-harm": true,
|
||||
"sensitive": true,
|
||||
}
|
||||
|
||||
func (srv *Server) WebPost(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
data := srv.NewTemplateContext()
|
||||
@@ -512,19 +524,15 @@ func (srv *Server) WebPost(c echo.Context) error {
|
||||
postView := tpv.Thread.FeedDefs_ThreadViewPost.Post
|
||||
data["postView"] = postView
|
||||
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
|
||||
if postView.Embed != nil {
|
||||
if postView.Embed.EmbedImages_View != nil {
|
||||
var thumbUrls []string
|
||||
for i := range postView.Embed.EmbedImages_View.Images {
|
||||
thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb)
|
||||
}
|
||||
data["imgThumbUrls"] = thumbUrls
|
||||
} else if postView.Embed.EmbedRecordWithMedia_View != nil && postView.Embed.EmbedRecordWithMedia_View.Media != nil && postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View != nil {
|
||||
var thumbUrls []string
|
||||
for i := range postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images {
|
||||
thumbUrls = append(thumbUrls, postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images[i].Thumb)
|
||||
}
|
||||
data["imgThumbUrls"] = thumbUrls
|
||||
|
||||
// If any undesirable labels are set, the embed will not be included in
|
||||
// metadata
|
||||
isEmbedHidden := false
|
||||
for _, label := range postView.Labels {
|
||||
isNeg := label.Neg != nil && *label.Neg
|
||||
if hideEmbedLabels[label.Val] && !isNeg {
|
||||
isEmbedHidden = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,6 +540,34 @@ func (srv *Server) WebPost(c echo.Context) error {
|
||||
postRecord, ok := postView.Record.Val.(*appbsky.FeedPost)
|
||||
if ok {
|
||||
data["postText"] = ExpandPostText(postRecord)
|
||||
|
||||
if !isEmbedHidden && postRecord.Labels != nil && postRecord.Labels.LabelDefs_SelfLabels != nil {
|
||||
for _, label := range postRecord.Labels.LabelDefs_SelfLabels.Values {
|
||||
if hideEmbedLabels[label.Val] {
|
||||
isEmbedHidden = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if postView.Embed != nil && !isEmbedHidden {
|
||||
hasImages := postView.Embed.EmbedImages_View != nil
|
||||
hasMedia := postView.Embed.EmbedRecordWithMedia_View != nil && postView.Embed.EmbedRecordWithMedia_View.Media != nil && postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View != nil
|
||||
|
||||
if hasImages {
|
||||
var thumbUrls []string
|
||||
for i := range postView.Embed.EmbedImages_View.Images {
|
||||
thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb)
|
||||
}
|
||||
data["imgThumbUrls"] = thumbUrls
|
||||
} else if hasMedia {
|
||||
var thumbUrls []string
|
||||
for i := range postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images {
|
||||
thumbUrls = append(thumbUrls, postView.Embed.EmbedRecordWithMedia_View.Media.EmbedImages_View.Images[i].Thumb)
|
||||
}
|
||||
data["imgThumbUrls"] = thumbUrls
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,46 @@
|
||||
<meta property="article:published_time" content="{{ postView.IndexedAt }}">
|
||||
<link rel="alternate" type="application/json+oembed" href="https://embed.bsky.app/oembed?format=json&url={{ postView.Uri | urlencode }}" />
|
||||
<link rel="alternate" href="{{ postView.Uri }}" />
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "DiscussionForumPosting",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
{%- if postView.Author.DisplayName %}
|
||||
"name": "{{ postView.Author.DisplayName }}",
|
||||
"alternateName": "@{{ postView.Author.Handle }}",
|
||||
{% else %}
|
||||
"name": "@{{ postView.Author.Handle }}",
|
||||
{% endif -%}
|
||||
"url": "https://bsky.app/profile/{{ postView.Author.Handle }}"
|
||||
},
|
||||
{%- if postText %}
|
||||
"text": "{{ postText }}",
|
||||
{% endif %}
|
||||
{%- if imageThumbUrls %}
|
||||
"image": "{{ imageThumbUrls[0] }}",
|
||||
{% endif %}
|
||||
"datePublished": "{{ postView.IndexedAt }}",
|
||||
"interactionStatistic": [
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/LikeAction",
|
||||
"userInteractionCount": {{ postView.LikeCount }}
|
||||
},
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/CommentAction",
|
||||
"userInteractionCount": {{ postView.ReplyCount }}
|
||||
},
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/ShareAction",
|
||||
"userInteractionCount": {{ postView.RepostCount + postView.QuoteCount }}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{%- elif requiresAuth and profileHandle -%}
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="profile:username" content="{{ profileHandle }}">
|
||||
|
||||
@@ -51,6 +51,45 @@
|
||||
<meta property="og:description" content="This profile requires authentication to view.">
|
||||
<meta property="twitter:description" content="This profile requires authentication to view.">
|
||||
{% endif %}
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfilePage",
|
||||
"dateCreated": "{{ profileView.CreatedAt }}",
|
||||
"mainEntity": {
|
||||
"@type": "Person",
|
||||
{%- if profileView.DisplayName %}
|
||||
"name": "{{ profileView.DisplayName }}",
|
||||
"alternateName": "@{{ profileView.Handle }}",
|
||||
{% else %}
|
||||
"name": "@{{ profileView.Handle }}",
|
||||
{% endif -%}
|
||||
"identifier": "{{ profileView.Did }}",
|
||||
"description": "{{ profileView.Description }}",
|
||||
"image": "{{ profileView.Avatar }}",
|
||||
"interactionStatistic": [
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/FollowAction",
|
||||
"userInteractionCount": {{ profileView.FollowersCount }}
|
||||
}
|
||||
],
|
||||
"agentInteractionStatistic": [
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/FollowAction",
|
||||
"userInteractionCount": {{ profileView.FollowsCount }}
|
||||
},
|
||||
{
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": "https://schema.org/WriteAction",
|
||||
"userInteractionCount": {{ profileView.PostsCount }}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endif -%}
|
||||
{%- endblock %}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ After you do `yarn ios` and `yarn android` once, you can later just run `yarn we
|
||||
|
||||
- Start in various console tabs:
|
||||
- `yarn e2e:mock-server`
|
||||
- `yarn e2e:metro`
|
||||
- `yarn e2e:start`
|
||||
- Run once: `yarn e2e:build`
|
||||
- Each test run: `yarn e2e:run`
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ values.
|
||||
### Running Maestro tests
|
||||
|
||||
- In one tab, run `yarn e2e:mock-server`
|
||||
- In a second tab, run `yarn e2e:metro`
|
||||
- In a second tab, run `yarn e2e:build`
|
||||
- In a third tab, run `yarn e2e:run`
|
||||
|
||||
## Using Flashlight for Performance Testing
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"extends": "base",
|
||||
"ios": {
|
||||
"resourceClass": "large",
|
||||
"autoIncrement": true
|
||||
"autoIncrement": true,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
@@ -50,7 +51,8 @@
|
||||
"extends": "base",
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"autoIncrement": false
|
||||
"autoIncrement": false,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": false
|
||||
@@ -63,7 +65,8 @@
|
||||
"testflight": {
|
||||
"extends": "base",
|
||||
"ios": {
|
||||
"autoIncrement": true
|
||||
"autoIncrement": true,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
@@ -77,7 +80,8 @@
|
||||
"extends": "base",
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"autoIncrement": true
|
||||
"autoIncrement": true,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import React from 'react'
|
||||
import {render} from '@testing-library/react-native'
|
||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {RootStoreProvider, RootStoreModel} from '../src/state'
|
||||
import {render} from '@testing-library/react-native'
|
||||
|
||||
import {ThemeProvider} from '../src/lib/ThemeContext'
|
||||
import {type RootStoreModel, RootStoreProvider} from '../src/state'
|
||||
|
||||
const customRender = (ui: any, rootStore: RootStoreModel) =>
|
||||
render(
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
<GestureHandlerRootView style={{flex: 1}}>
|
||||
<RootSiblingParent>
|
||||
<RootStoreProvider value={rootStore}>
|
||||
<ThemeProvider theme="light">
|
||||
<SafeAreaProvider>{ui}</SafeAreaProvider>
|
||||
</ThemeProvider>
|
||||
</RootStoreProvider>
|
||||
</RootSiblingParent>
|
||||
<RootStoreProvider value={rootStore}>
|
||||
<ThemeProvider theme="light">
|
||||
<SafeAreaProvider>{ui}</SafeAreaProvider>
|
||||
</ThemeProvider>
|
||||
</RootStoreProvider>
|
||||
</GestureHandlerRootView>,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.109.0",
|
||||
"version": "1.110.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -46,8 +46,9 @@
|
||||
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
|
||||
"typecheck": "tsc --project ./tsconfig.check.json",
|
||||
"e2e:mock-server": "NODE_ENV=development ./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
|
||||
"e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
||||
"e2e:metro-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
|
||||
"e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
||||
"e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
|
||||
"e2e:start": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo start",
|
||||
"e2e:run": "maestro test __e2e__",
|
||||
"perf:test": "NODE_ENV=test maestro test",
|
||||
"perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml",
|
||||
@@ -71,9 +72,11 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.16.7",
|
||||
"@atproto/api": "^0.17.6",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@bsky.app/alf": "^0.1.5",
|
||||
"@bsky.app/react-native-mmkv": "2.12.5",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@expo/html-elements": "^0.12.5",
|
||||
@@ -128,36 +131,37 @@
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "^54.0.9",
|
||||
"expo": "^54.0.22",
|
||||
"expo-application": "~7.0.7",
|
||||
"expo-blur": "~15.0.7",
|
||||
"expo-build-properties": "~1.0.9",
|
||||
"expo-camera": "~17.0.8",
|
||||
"expo-camera": "~17.0.9",
|
||||
"expo-clipboard": "~8.0.7",
|
||||
"expo-dev-client": "~6.0.12",
|
||||
"expo-device": "~8.0.8",
|
||||
"expo-file-system": "~19.0.14",
|
||||
"expo-font": "~14.0.8",
|
||||
"expo-dev-client": "~6.0.16",
|
||||
"expo-device": "~8.0.9",
|
||||
"expo-file-system": "~19.0.17",
|
||||
"expo-font": "~14.0.9",
|
||||
"expo-haptics": "~15.0.7",
|
||||
"expo-image": "~3.0.8",
|
||||
"expo-image": "~3.0.10",
|
||||
"expo-image-crop-tool": "^0.1.8",
|
||||
"expo-image-manipulator": "~14.0.7",
|
||||
"expo-image-picker": "~17.0.8",
|
||||
"expo-intent-launcher": "~13.0.7",
|
||||
"expo-keep-awake": "^15.0.7",
|
||||
"expo-linear-gradient": "~15.0.7",
|
||||
"expo-linking": "~8.0.8",
|
||||
"expo-localization": "~17.0.7",
|
||||
"expo-location": "~19.0.7",
|
||||
"expo-media-library": "~18.2.0",
|
||||
"expo-notifications": "~0.32.11",
|
||||
"expo-notifications": "~0.32.12",
|
||||
"expo-screen-orientation": "~9.0.7",
|
||||
"expo-sharing": "~14.0.7",
|
||||
"expo-splash-screen": "~31.0.10",
|
||||
"expo-system-ui": "~6.0.7",
|
||||
"expo-task-manager": "~14.0.7",
|
||||
"expo-updates": "~29.0.11",
|
||||
"expo-video": "~3.0.11",
|
||||
"expo-web-browser": "~15.0.7",
|
||||
"expo-system-ui": "~6.0.8",
|
||||
"expo-task-manager": "~14.0.8",
|
||||
"expo-updates": "~29.0.12",
|
||||
"expo-video": "~3.0.13",
|
||||
"expo-web-browser": "~15.0.9",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
"hls.js": "^1.6.2",
|
||||
@@ -182,7 +186,7 @@
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-is": "19",
|
||||
"react-keyed-flatten-children": "^5.0.0",
|
||||
"react-native": "0.81.4",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-compressor": "^1.13.0",
|
||||
"react-native-date-picker": "^5.0.13",
|
||||
"react-native-device-attest": "^0.1.6",
|
||||
@@ -191,12 +195,10 @@
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-keyboard-controller": "1.18.5",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.8.0",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
"react-native-reanimated": "^3.19.1",
|
||||
"react-native-root-siblings": "^5.0.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-svg": "15.12.1",
|
||||
@@ -219,7 +221,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.172",
|
||||
"@atproto/dev-env": "^0.3.181",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
@@ -227,13 +229,13 @@
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@react-native/babel-preset": "0.81.4",
|
||||
"@react-native/eslint-config": "^0.81.4",
|
||||
"@react-native/typescript-config": "^0.81.4",
|
||||
"@react-native/babel-preset": "0.81.5",
|
||||
"@react-native/eslint-config": "^0.81.5",
|
||||
"@react-native/typescript-config": "^0.81.5",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.2.0",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash.chunk": "^4.2.7",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.isequal": "^4.5.6",
|
||||
@@ -246,7 +248,7 @@
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.3",
|
||||
"babel-preset-expo": "~54.0.0",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-plugin-bsky-internal": "link:./eslint",
|
||||
@@ -254,14 +256,14 @@
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-lingui": "^0.2.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"eslint-plugin-react-native-a11y": "^3.3.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"file-loader": "6.2.0",
|
||||
"husky": "^8.0.3",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~54.0.12",
|
||||
"jest-expo": "~54.0.13",
|
||||
"jest-junit": "^16.0.0",
|
||||
"lint-staged": "^13.2.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
@@ -271,12 +273,13 @@
|
||||
"react-refresh": "^0.14.0",
|
||||
"svgo": "^3.3.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-plugin-sort-import-suggestions": "^1.0.4",
|
||||
"typescript": "^5.9.2",
|
||||
"webpack-bundle-analyzer": "^4.10.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@react-native/babel-preset": "0.81.4",
|
||||
"@react-native/normalize-colors": "0.81.4",
|
||||
"@react-native/babel-preset": "0.81.5",
|
||||
"@react-native/normalize-colors": "0.81.5",
|
||||
"**/@expo/image-utils": "0.8.7",
|
||||
"**/expo-constants": "18.0.8",
|
||||
"**/expo-device": "7.1.4",
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
diff --git a/node_modules/react-native-compressor/android/build.gradle b/node_modules/react-native-compressor/android/build.gradle
|
||||
index 5071139..84bee34 100644
|
||||
--- a/node_modules/react-native-compressor/android/build.gradle
|
||||
+++ b/node_modules/react-native-compressor/android/build.gradle
|
||||
@@ -115,7 +115,6 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
|
||||
implementation 'org.mp4parser:isoparser:1.9.56'
|
||||
- implementation 'com.github.banketree:AndroidLame-kotlin:v0.0.1'
|
||||
implementation 'javazoom:jlayer:1.0.1'
|
||||
}
|
||||
|
||||
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt
|
||||
deleted file mode 100644
|
||||
index 9292d3e..0000000
|
||||
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt
|
||||
+++ /dev/null
|
||||
@@ -1,264 +0,0 @@
|
||||
-package com.reactnativecompressor.Audio
|
||||
-
|
||||
-
|
||||
-import android.annotation.SuppressLint
|
||||
-import com.facebook.react.bridge.Promise
|
||||
-import com.facebook.react.bridge.ReactApplicationContext
|
||||
-import com.facebook.react.bridge.ReadableMap
|
||||
-import com.naman14.androidlame.LameBuilder
|
||||
-import com.naman14.androidlame.WaveReader
|
||||
-import com.reactnativecompressor.Utils.MediaCache
|
||||
-import com.reactnativecompressor.Utils.Utils
|
||||
-import com.reactnativecompressor.Utils.Utils.addLog
|
||||
-import javazoom.jl.converter.Converter
|
||||
-import javazoom.jl.decoder.JavaLayerException
|
||||
-import java.io.BufferedOutputStream
|
||||
-import java.io.File
|
||||
-import java.io.FileNotFoundException
|
||||
-import java.io.FileOutputStream
|
||||
-import java.io.IOException
|
||||
-
|
||||
-class AudioCompressor {
|
||||
- companion object {
|
||||
- val TAG="AudioMain"
|
||||
- private const val OUTPUT_STREAM_BUFFER = 8192
|
||||
-
|
||||
- var outputStream: BufferedOutputStream? = null
|
||||
- var waveReader: WaveReader? = null
|
||||
- @JvmStatic
|
||||
- fun CompressAudio(
|
||||
- fileUrl: String,
|
||||
- optionMap: ReadableMap,
|
||||
- context: ReactApplicationContext,
|
||||
- promise: Promise,
|
||||
- ) {
|
||||
- val realPath = Utils.getRealPath(fileUrl, context)
|
||||
- var _fileUrl=realPath
|
||||
- val filePathWithoutFileUri = realPath!!.replace("file://", "")
|
||||
- try {
|
||||
- var wavPath=filePathWithoutFileUri;
|
||||
- var isNonWav:Boolean=false
|
||||
- if (fileUrl.endsWith(".mp4", ignoreCase = true))
|
||||
- {
|
||||
- addLog("mp4 file found")
|
||||
- val mp3Path= Utils.generateCacheFilePath("mp3", context)
|
||||
- AudioExtractor().genVideoUsingMuxer(fileUrl, mp3Path, -1, -1, true, false)
|
||||
- _fileUrl=Utils.slashifyFilePath(mp3Path)
|
||||
- wavPath= Utils.generateCacheFilePath("wav", context)
|
||||
- try {
|
||||
- val converter = Converter()
|
||||
- converter.convert(mp3Path, wavPath)
|
||||
- } catch (e: JavaLayerException) {
|
||||
- addLog("JavaLayerException error"+e.localizedMessage)
|
||||
- e.printStackTrace();
|
||||
- }
|
||||
- isNonWav=true
|
||||
- }
|
||||
- else if (!fileUrl.endsWith(".wav", ignoreCase = true))
|
||||
- {
|
||||
- addLog("non wav file found")
|
||||
- wavPath= Utils.generateCacheFilePath("wav", context)
|
||||
- try {
|
||||
- val converter = Converter()
|
||||
- converter.convert(filePathWithoutFileUri, wavPath)
|
||||
- } catch (e: JavaLayerException) {
|
||||
- addLog("JavaLayerException error"+e.localizedMessage)
|
||||
- e.printStackTrace();
|
||||
- }
|
||||
- isNonWav=true
|
||||
- }
|
||||
-
|
||||
-
|
||||
- autoCompressHelper(wavPath,filePathWithoutFileUri, optionMap,context) { mp3Path, finished ->
|
||||
- if (finished) {
|
||||
- val returnableFilePath:String="file://$mp3Path"
|
||||
- addLog("finished: " + returnableFilePath)
|
||||
- MediaCache.removeCompletedImagePath(fileUrl)
|
||||
- if(isNonWav)
|
||||
- {
|
||||
- File(wavPath).delete()
|
||||
- }
|
||||
- promise.resolve(returnableFilePath)
|
||||
- } else {
|
||||
- addLog("error: "+mp3Path)
|
||||
- promise.resolve(_fileUrl)
|
||||
- }
|
||||
- }
|
||||
- } catch (e: Exception) {
|
||||
- promise.resolve(_fileUrl)
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- @SuppressLint("WrongConstant")
|
||||
- private fun autoCompressHelper(
|
||||
- fileUrl: String,
|
||||
- actualFileUrl: String,
|
||||
- optionMap: ReadableMap,
|
||||
- context: ReactApplicationContext,
|
||||
- completeCallback: (String, Boolean) -> Unit
|
||||
- ) {
|
||||
-
|
||||
- val options = AudioHelper.fromMap(optionMap)
|
||||
- val quality = options.quality
|
||||
-
|
||||
- var isCompletedCallbackTriggered:Boolean=false
|
||||
- try {
|
||||
- var mp3Path = Utils.generateCacheFilePath("mp3", context)
|
||||
- val input = File(fileUrl)
|
||||
- val output = File(mp3Path)
|
||||
-
|
||||
- val CHUNK_SIZE = 8192
|
||||
- addLog("Initialising wav reader")
|
||||
-
|
||||
- waveReader = WaveReader(input)
|
||||
-
|
||||
- try {
|
||||
- waveReader!!.openWave()
|
||||
- } catch (e: IOException) {
|
||||
- e.printStackTrace()
|
||||
- }
|
||||
-
|
||||
- addLog("Intitialising encoder")
|
||||
-
|
||||
-
|
||||
- // for bitrate
|
||||
- var audioBitrate:Int
|
||||
- if(options.bitrate != -1)
|
||||
- {
|
||||
- audioBitrate= options.bitrate/1000
|
||||
- }
|
||||
- else
|
||||
- {
|
||||
- audioBitrate=AudioHelper.getDestinationBitrateByQuality(actualFileUrl, quality!!)
|
||||
- Utils.addLog("dest bitrate: $audioBitrate")
|
||||
- }
|
||||
-
|
||||
- var androidLame = LameBuilder();
|
||||
- androidLame.setOutBitrate(audioBitrate)
|
||||
-
|
||||
- // for channels
|
||||
- var audioChannels:Int
|
||||
- if(options.channels != -1){
|
||||
- audioChannels= options.channels!!
|
||||
- }
|
||||
- else
|
||||
- {
|
||||
- audioChannels=waveReader!!.channels
|
||||
- }
|
||||
- androidLame.setOutChannels(audioChannels)
|
||||
-
|
||||
- // for sample rate
|
||||
- androidLame.setInSampleRate(waveReader!!.sampleRate)
|
||||
- var audioSampleRate:Int
|
||||
- if(options.samplerate != -1){
|
||||
- audioSampleRate= options.samplerate!!
|
||||
- }
|
||||
- else
|
||||
- {
|
||||
- audioSampleRate=waveReader!!.sampleRate
|
||||
- }
|
||||
- androidLame.setOutSampleRate(audioSampleRate)
|
||||
- val androidLameBuild=androidLame.build()
|
||||
-
|
||||
- try {
|
||||
- outputStream = BufferedOutputStream(FileOutputStream(output), OUTPUT_STREAM_BUFFER)
|
||||
- } catch (e: FileNotFoundException) {
|
||||
- e.printStackTrace()
|
||||
- }
|
||||
-
|
||||
- var bytesRead = 0
|
||||
-
|
||||
- val buffer_l = ShortArray(CHUNK_SIZE)
|
||||
- val buffer_r = ShortArray(CHUNK_SIZE)
|
||||
- val mp3Buf = ByteArray(CHUNK_SIZE)
|
||||
-
|
||||
- val channels = waveReader!!.channels
|
||||
-
|
||||
- addLog("started encoding")
|
||||
- while (true) {
|
||||
- try {
|
||||
- if (channels == 2) {
|
||||
-
|
||||
- bytesRead = waveReader!!.read(buffer_l, buffer_r, CHUNK_SIZE)
|
||||
- addLog("bytes read=$bytesRead")
|
||||
-
|
||||
- if (bytesRead > 0) {
|
||||
-
|
||||
- var bytesEncoded = 0
|
||||
- bytesEncoded = androidLameBuild.encode(buffer_l, buffer_r, bytesRead, mp3Buf)
|
||||
- addLog("bytes encoded=$bytesEncoded")
|
||||
-
|
||||
- if (bytesEncoded > 0) {
|
||||
- try {
|
||||
- addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
|
||||
- outputStream!!.write(mp3Buf, 0, bytesEncoded)
|
||||
- } catch (e: IOException) {
|
||||
- e.printStackTrace()
|
||||
- }
|
||||
-
|
||||
- }
|
||||
-
|
||||
- } else
|
||||
- break
|
||||
- } else {
|
||||
-
|
||||
- bytesRead = waveReader!!.read(buffer_l, CHUNK_SIZE)
|
||||
- addLog("bytes read=$bytesRead")
|
||||
-
|
||||
- if (bytesRead > 0) {
|
||||
- var bytesEncoded = 0
|
||||
-
|
||||
- bytesEncoded = androidLameBuild.encode(buffer_l, buffer_l, bytesRead, mp3Buf)
|
||||
- addLog("bytes encoded=$bytesEncoded")
|
||||
-
|
||||
- if (bytesEncoded > 0) {
|
||||
- try {
|
||||
- addLog("writing mp3 buffer to outputstream with $bytesEncoded bytes")
|
||||
- outputStream!!.write(mp3Buf, 0, bytesEncoded)
|
||||
- } catch (e: IOException) {
|
||||
- e.printStackTrace()
|
||||
- }
|
||||
-
|
||||
- }
|
||||
-
|
||||
- } else
|
||||
- break
|
||||
- }
|
||||
-
|
||||
-
|
||||
- } catch (e: IOException) {
|
||||
- e.printStackTrace()
|
||||
- }
|
||||
-
|
||||
- }
|
||||
-
|
||||
- addLog("flushing final mp3buffer")
|
||||
- val outputMp3buf = androidLameBuild.flush(mp3Buf)
|
||||
- addLog("flushed $outputMp3buf bytes")
|
||||
- if (outputMp3buf > 0) {
|
||||
- try {
|
||||
- addLog("writing final mp3buffer to outputstream")
|
||||
- outputStream!!.write(mp3Buf, 0, outputMp3buf)
|
||||
- addLog("closing output stream")
|
||||
- outputStream!!.close()
|
||||
- completeCallback(output.absolutePath, true)
|
||||
- isCompletedCallbackTriggered=true
|
||||
- } catch (e: IOException) {
|
||||
- completeCallback(e.localizedMessage, false)
|
||||
- e.printStackTrace()
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- } catch (e: IOException) {
|
||||
- completeCallback(e.localizedMessage, false)
|
||||
- }
|
||||
- if(!isCompletedCallbackTriggered)
|
||||
- {
|
||||
- completeCallback("something went wrong", false)
|
||||
- }
|
||||
- }
|
||||
-
|
||||
-
|
||||
-
|
||||
- }
|
||||
-}
|
||||
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt
|
||||
deleted file mode 100644
|
||||
index c655182..0000000
|
||||
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt
|
||||
+++ /dev/null
|
||||
@@ -1,112 +0,0 @@
|
||||
-package com.reactnativecompressor.Audio
|
||||
-
|
||||
-import android.annotation.SuppressLint
|
||||
-import android.media.MediaCodec
|
||||
-import android.media.MediaExtractor
|
||||
-import android.media.MediaFormat
|
||||
-import android.media.MediaMetadataRetriever
|
||||
-import android.media.MediaMuxer
|
||||
-import android.util.Log
|
||||
-import java.io.IOException
|
||||
-import java.nio.ByteBuffer
|
||||
-
|
||||
-
|
||||
-class AudioExtractor {
|
||||
- /**
|
||||
- * @param srcPath the path of source video file.
|
||||
- * @param dstPath the path of destination video file.
|
||||
- * @param startMs starting time in milliseconds for trimming. Set to
|
||||
- * negative if starting from beginning.
|
||||
- * @param endMs end time for trimming in milliseconds. Set to negative if
|
||||
- * no trimming at the end.
|
||||
- * @param useAudio true if keep the audio track from the source.
|
||||
- * @param useVideo true if keep the video track from the source.
|
||||
- * @throws IOException
|
||||
- */
|
||||
- @SuppressLint("NewApi", "WrongConstant")
|
||||
- @Throws(IOException::class)
|
||||
- fun genVideoUsingMuxer(srcPath: String?, dstPath: String?, startMs: Int, endMs: Int, useAudio: Boolean, useVideo: Boolean) {
|
||||
- // Set up MediaExtractor to read from the source.
|
||||
- val extractor = MediaExtractor()
|
||||
- extractor.setDataSource(srcPath!!)
|
||||
- val trackCount = extractor.trackCount
|
||||
- // Set up MediaMuxer for the destination.
|
||||
- val muxer: MediaMuxer
|
||||
- muxer = MediaMuxer(dstPath!!, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
|
||||
- // Set up the tracks and retrieve the max buffer size for selected
|
||||
- // tracks.
|
||||
- val indexMap = HashMap<Int, Int>(trackCount)
|
||||
- var bufferSize = -1
|
||||
- for (i in 0 until trackCount) {
|
||||
- val format = extractor.getTrackFormat(i)
|
||||
- val mime = format.getString(MediaFormat.KEY_MIME)
|
||||
- var selectCurrentTrack = false
|
||||
- if (mime!!.startsWith("audio/") && useAudio) {
|
||||
- selectCurrentTrack = true
|
||||
- } else if (mime.startsWith("video/") && useVideo) {
|
||||
- selectCurrentTrack = true
|
||||
- }
|
||||
- if (selectCurrentTrack) {
|
||||
- extractor.selectTrack(i)
|
||||
- val dstIndex = muxer.addTrack(format)
|
||||
- indexMap[i] = dstIndex
|
||||
- if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
|
||||
- val newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
|
||||
- bufferSize = if (newSize > bufferSize) newSize else bufferSize
|
||||
- }
|
||||
- }
|
||||
- }
|
||||
- if (bufferSize < 0) {
|
||||
- bufferSize = DEFAULT_BUFFER_SIZE
|
||||
- }
|
||||
- // Set up the orientation and starting time for extractor.
|
||||
- val retrieverSrc = MediaMetadataRetriever()
|
||||
- retrieverSrc.setDataSource(srcPath)
|
||||
- val degreesString = retrieverSrc.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
||||
- if (degreesString != null) {
|
||||
- val degrees = degreesString.toInt()
|
||||
- if (degrees >= 0) {
|
||||
- muxer.setOrientationHint(degrees)
|
||||
- }
|
||||
- }
|
||||
- if (startMs > 0) {
|
||||
- extractor.seekTo((startMs * 1000).toLong(), MediaExtractor.SEEK_TO_CLOSEST_SYNC)
|
||||
- }
|
||||
- // Copy the samples from MediaExtractor to MediaMuxer. We will loop
|
||||
- // for copying each sample and stop when we get to the end of the source
|
||||
- // file or exceed the end time of the trimming.
|
||||
- val offset = 0
|
||||
- var trackIndex = -1
|
||||
- val dstBuf = ByteBuffer.allocate(bufferSize)
|
||||
- val bufferInfo = MediaCodec.BufferInfo()
|
||||
- muxer.start()
|
||||
- while (true) {
|
||||
- bufferInfo.offset = offset
|
||||
- bufferInfo.size = extractor.readSampleData(dstBuf, offset)
|
||||
- if (bufferInfo.size < 0) {
|
||||
- Log.d(TAG, "Saw input EOS.")
|
||||
- bufferInfo.size = 0
|
||||
- break
|
||||
- } else {
|
||||
- bufferInfo.presentationTimeUs = extractor.sampleTime
|
||||
- if (endMs > 0 && bufferInfo.presentationTimeUs > endMs * 1000) {
|
||||
- Log.d(TAG, "The current sample is over the trim end time.")
|
||||
- break
|
||||
- } else {
|
||||
- bufferInfo.flags = extractor.sampleFlags
|
||||
- trackIndex = extractor.sampleTrackIndex
|
||||
- muxer.writeSampleData(indexMap[trackIndex]!!, dstBuf, bufferInfo)
|
||||
- extractor.advance()
|
||||
- }
|
||||
- }
|
||||
- }
|
||||
- muxer.stop()
|
||||
- muxer.release()
|
||||
- return
|
||||
- }
|
||||
-
|
||||
- companion object {
|
||||
- private const val DEFAULT_BUFFER_SIZE = 1 * 1024 * 1024
|
||||
- private const val TAG = "AudioExtractorDecoder"
|
||||
- }
|
||||
-}
|
||||
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt
|
||||
deleted file mode 100644
|
||||
index 42040b4..0000000
|
||||
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt
|
||||
+++ /dev/null
|
||||
@@ -1,72 +0,0 @@
|
||||
-package com.reactnativecompressor.Audio
|
||||
-
|
||||
-import android.media.MediaExtractor
|
||||
-import android.media.MediaFormat
|
||||
-import com.facebook.react.bridge.ReadableMap
|
||||
-import com.reactnativecompressor.Utils.Utils
|
||||
-import java.io.File
|
||||
-import java.io.IOException
|
||||
-
|
||||
-
|
||||
-class AudioHelper {
|
||||
-
|
||||
- var quality: String? = "medium"
|
||||
- var bitrate: Int = -1
|
||||
- var samplerate: Int = -1
|
||||
- var channels: Int = -1
|
||||
- var progressDivider: Int? = 0
|
||||
-
|
||||
- companion object {
|
||||
- fun fromMap(map: ReadableMap): AudioHelper {
|
||||
- val options = AudioHelper()
|
||||
- val iterator = map.keySetIterator()
|
||||
- while (iterator.hasNextKey()) {
|
||||
- val key = iterator.nextKey()
|
||||
- when (key) {
|
||||
- "quality" -> options.quality = map.getString(key)
|
||||
- "bitrate" -> {
|
||||
- val bitrate = map.getInt(key)
|
||||
- options.bitrate = if (bitrate > 320000 || bitrate < 64000) 64000 else bitrate
|
||||
- }
|
||||
- "samplerate" -> options.samplerate = map.getInt(key)
|
||||
- "channels" -> options.channels = map.getInt(key)
|
||||
- }
|
||||
- }
|
||||
- return options
|
||||
- }
|
||||
-
|
||||
-
|
||||
- fun getAudioBitrate(path: String): Int {
|
||||
- val file = File(path)
|
||||
- val fileSize = file.length() * 8 // size in bits
|
||||
-
|
||||
- val mex = MediaExtractor()
|
||||
- try {
|
||||
- mex.setDataSource(path)
|
||||
- } catch (e: IOException) {
|
||||
- e.printStackTrace()
|
||||
- }
|
||||
-
|
||||
- val mf = mex.getTrackFormat(0)
|
||||
- val durationUs = mf.getLong(MediaFormat.KEY_DURATION)
|
||||
- val durationSec = durationUs / 1_000_000.0 // convert duration to seconds
|
||||
-
|
||||
- return (fileSize / durationSec).toInt()/1000 // bitrate in bits per second
|
||||
- }
|
||||
- fun getDestinationBitrateByQuality(path: String, quality: String): Int {
|
||||
- val originalBitrate = getAudioBitrate(path)
|
||||
- var destinationBitrate = originalBitrate
|
||||
- Utils.addLog("source bitrate: $originalBitrate")
|
||||
-
|
||||
- when (quality.lowercase()) {
|
||||
- "low" -> destinationBitrate = maxOf(64, (originalBitrate * 0.3).toInt())
|
||||
- "medium" -> destinationBitrate = (originalBitrate * 0.5).toInt()
|
||||
- "high" -> destinationBitrate = minOf(320, (originalBitrate * 0.7).toInt())
|
||||
- else -> Utils.addLog("Invalid quality level. Please enter 'low', 'medium', or 'high'.")
|
||||
- }
|
||||
-
|
||||
- return destinationBitrate
|
||||
- }
|
||||
-
|
||||
- }
|
||||
-}
|
||||
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt
|
||||
index 446d4fb..f021909 100644
|
||||
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt
|
||||
+++ b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt
|
||||
@@ -11,7 +11,9 @@ class AudioMain(private val reactContext: ReactApplicationContext) {
|
||||
promise: Promise) {
|
||||
try {
|
||||
|
||||
- AudioCompressor.CompressAudio(fileUrl,optionMap,reactContext,promise)
|
||||
+ // Skip compression on Android to avoid libandroidlame dependency
|
||||
+ // Return the original file URL without compression
|
||||
+ promise.resolve(fileUrl)
|
||||
} catch (ex: Exception) {
|
||||
promise.reject(ex)
|
||||
}
|
||||
diff --git a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt
|
||||
index c14b727..1198908 100644
|
||||
--- a/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt
|
||||
+++ b/node_modules/react-native-compressor/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt
|
||||
@@ -7,7 +7,6 @@ import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
-import com.reactnativecompressor.Audio.AudioCompressor
|
||||
import com.reactnativecompressor.Video.VideoCompressor.CompressionListener
|
||||
import com.reactnativecompressor.Video.VideoCompressor.VideoCompressorClass
|
||||
import java.io.FileNotFoundException
|
||||
@@ -152,10 +151,6 @@ object Utils {
|
||||
}
|
||||
}
|
||||
|
||||
- fun addLog(log: String) {
|
||||
- Log.d(AudioCompressor.TAG, log)
|
||||
- }
|
||||
-
|
||||
val exifAttributes = arrayOf(
|
||||
"FNumber",
|
||||
"ApertureValue",
|
||||
@@ -0,0 +1,5 @@
|
||||
# react-native-compressor
|
||||
|
||||
Patch file taken from https://github.com/numandev1/react-native-compressor/pull/355#issuecomment-3180870738
|
||||
|
||||
This patch removes the audio compression feature on Android from the library. This is because `libandroidlame.so`, the native dependency, does not support 16kb page sizes, and the Play Store has made this mandatory as of 1st Nov 2025.
|
||||
@@ -0,0 +1,389 @@
|
||||
diff --git a/node_modules/react-native-reanimated/lib/module/component/PerformanceMonitor.js b/node_modules/react-native-reanimated/lib/module/component/PerformanceMonitor.js
|
||||
index 9c98d6c..70ada9f 100644
|
||||
--- a/node_modules/react-native-reanimated/lib/module/component/PerformanceMonitor.js
|
||||
+++ b/node_modules/react-native-reanimated/lib/module/component/PerformanceMonitor.js
|
||||
@@ -1,125 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
-import React, { useEffect, useRef } from 'react';
|
||||
-import { StyleSheet, TextInput, View } from 'react-native';
|
||||
-import { addWhitelistedNativeProps } from "../ConfigHelper.js";
|
||||
-import { createAnimatedComponent } from "../createAnimatedComponent/index.js";
|
||||
-import { useAnimatedProps, useFrameCallback, useSharedValue } from "../hook/index.js";
|
||||
-function createCircularDoublesBuffer(size) {
|
||||
- 'worklet';
|
||||
-
|
||||
- return {
|
||||
- next: 0,
|
||||
- buffer: new Float32Array(size),
|
||||
- size,
|
||||
- count: 0,
|
||||
- push(value) {
|
||||
- const oldValue = this.buffer[this.next];
|
||||
- const oldCount = this.count;
|
||||
- this.buffer[this.next] = value;
|
||||
- this.next = (this.next + 1) % this.size;
|
||||
- this.count = Math.min(this.size, this.count + 1);
|
||||
- return oldCount === this.size ? oldValue : null;
|
||||
- },
|
||||
- front() {
|
||||
- const notEmpty = this.count > 0;
|
||||
- if (notEmpty) {
|
||||
- const current = this.next - 1;
|
||||
- const index = current < 0 ? this.size - 1 : current;
|
||||
- return this.buffer[index];
|
||||
- }
|
||||
- return null;
|
||||
- },
|
||||
- back() {
|
||||
- const notEmpty = this.count > 0;
|
||||
- return notEmpty ? this.buffer[this.next] : null;
|
||||
- }
|
||||
- };
|
||||
-}
|
||||
-const DEFAULT_BUFFER_SIZE = 20;
|
||||
-addWhitelistedNativeProps({
|
||||
- text: true
|
||||
-});
|
||||
-const AnimatedTextInput = createAnimatedComponent(TextInput);
|
||||
-function loopAnimationFrame(fn) {
|
||||
- let lastTime = 0;
|
||||
- function loop() {
|
||||
- requestAnimationFrame(time => {
|
||||
- if (lastTime > 0) {
|
||||
- fn(lastTime, time);
|
||||
- }
|
||||
- lastTime = time;
|
||||
- requestAnimationFrame(loop);
|
||||
- });
|
||||
- }
|
||||
- loop();
|
||||
-}
|
||||
-function getFps(renderTimeInMs) {
|
||||
- 'worklet';
|
||||
-
|
||||
- return 1000 / renderTimeInMs;
|
||||
-}
|
||||
-function completeBufferRoutine(buffer, timestamp) {
|
||||
- 'worklet';
|
||||
-
|
||||
- timestamp = Math.round(timestamp);
|
||||
- const droppedTimestamp = buffer.push(timestamp) ?? timestamp;
|
||||
- const measuredRangeDuration = timestamp - droppedTimestamp;
|
||||
- return getFps(measuredRangeDuration / buffer.count);
|
||||
-}
|
||||
-function JsPerformance({
|
||||
- smoothingFrames
|
||||
-}) {
|
||||
- const jsFps = useSharedValue(null);
|
||||
- const totalRenderTime = useSharedValue(0);
|
||||
- const circularBuffer = useRef(createCircularDoublesBuffer(smoothingFrames));
|
||||
- useEffect(() => {
|
||||
- loopAnimationFrame((_, timestamp) => {
|
||||
- timestamp = Math.round(timestamp);
|
||||
- const currentFps = completeBufferRoutine(circularBuffer.current, timestamp);
|
||||
-
|
||||
- // JS fps have to be measured every 2nd frame,
|
||||
- // thus 2x multiplication has to occur here
|
||||
- jsFps.value = (currentFps * 2).toFixed(0);
|
||||
- });
|
||||
- }, [jsFps, totalRenderTime]);
|
||||
- const animatedProps = useAnimatedProps(() => {
|
||||
- const text = 'JS: ' + (jsFps.value ?? 'N/A') + ' ';
|
||||
- return {
|
||||
- text,
|
||||
- defaultValue: text
|
||||
- };
|
||||
- });
|
||||
- return <View style={styles.container}>
|
||||
- <AnimatedTextInput style={styles.text} animatedProps={animatedProps} editable={false} />
|
||||
- </View>;
|
||||
-}
|
||||
-function UiPerformance({
|
||||
- smoothingFrames
|
||||
-}) {
|
||||
- const uiFps = useSharedValue(null);
|
||||
- const circularBuffer = useSharedValue(null);
|
||||
- useFrameCallback(({
|
||||
- timestamp
|
||||
- }) => {
|
||||
- if (circularBuffer.value === null) {
|
||||
- circularBuffer.value = createCircularDoublesBuffer(smoothingFrames);
|
||||
- }
|
||||
- timestamp = Math.round(timestamp);
|
||||
- const currentFps = completeBufferRoutine(circularBuffer.value, timestamp);
|
||||
- uiFps.value = currentFps.toFixed(0);
|
||||
- });
|
||||
- const animatedProps = useAnimatedProps(() => {
|
||||
- const text = 'UI: ' + (uiFps.value ?? 'N/A') + ' ';
|
||||
- return {
|
||||
- text,
|
||||
- defaultValue: text
|
||||
- };
|
||||
- });
|
||||
- return <View style={styles.container}>
|
||||
- <AnimatedTextInput style={styles.text} animatedProps={animatedProps} editable={false} />
|
||||
- </View>;
|
||||
-}
|
||||
/**
|
||||
* A component that lets you measure fps values on JS and UI threads on both the
|
||||
* Paper and Fabric architectures.
|
||||
@@ -127,38 +7,7 @@ function UiPerformance({
|
||||
* @param smoothingFrames - Determines amount of saved frames which will be used
|
||||
* for fps value smoothing.
|
||||
*/
|
||||
-export function PerformanceMonitor({
|
||||
- smoothingFrames = DEFAULT_BUFFER_SIZE
|
||||
-}) {
|
||||
- return <View style={styles.monitor}>
|
||||
- <JsPerformance smoothingFrames={smoothingFrames} />
|
||||
- <UiPerformance smoothingFrames={smoothingFrames} />
|
||||
- </View>;
|
||||
+export function PerformanceMonitor() {
|
||||
+ return null;
|
||||
}
|
||||
-const styles = StyleSheet.create({
|
||||
- monitor: {
|
||||
- flexDirection: 'row',
|
||||
- position: 'absolute',
|
||||
- backgroundColor: '#0006',
|
||||
- zIndex: 1000
|
||||
- },
|
||||
- header: {
|
||||
- fontSize: 14,
|
||||
- color: '#ffff',
|
||||
- paddingHorizontal: 5
|
||||
- },
|
||||
- text: {
|
||||
- fontSize: 13,
|
||||
- fontVariant: ['tabular-nums'],
|
||||
- color: '#ffff',
|
||||
- fontFamily: 'monospace',
|
||||
- paddingHorizontal: 3
|
||||
- },
|
||||
- container: {
|
||||
- alignItems: 'center',
|
||||
- justifyContent: 'center',
|
||||
- flexDirection: 'row',
|
||||
- flexWrap: 'wrap'
|
||||
- }
|
||||
-});
|
||||
//# sourceMappingURL=PerformanceMonitor.js.map
|
||||
diff --git a/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx b/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx
|
||||
index ff8fc8a..34dde79 100644
|
||||
--- a/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx
|
||||
+++ b/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx
|
||||
@@ -1,170 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
-import React, { useEffect, useRef } from 'react';
|
||||
-import { StyleSheet, TextInput, View } from 'react-native';
|
||||
-
|
||||
-import { addWhitelistedNativeProps } from '../ConfigHelper';
|
||||
-import { createAnimatedComponent } from '../createAnimatedComponent';
|
||||
-import type { FrameInfo } from '../frameCallback';
|
||||
-import { useAnimatedProps, useFrameCallback, useSharedValue } from '../hook';
|
||||
-
|
||||
-type CircularBuffer = ReturnType<typeof createCircularDoublesBuffer>;
|
||||
-function createCircularDoublesBuffer(size: number) {
|
||||
- 'worklet';
|
||||
-
|
||||
- return {
|
||||
- next: 0 as number,
|
||||
- buffer: new Float32Array(size),
|
||||
- size,
|
||||
- count: 0 as number,
|
||||
-
|
||||
- push(value: number): number | null {
|
||||
- const oldValue = this.buffer[this.next];
|
||||
- const oldCount = this.count;
|
||||
- this.buffer[this.next] = value;
|
||||
-
|
||||
- this.next = (this.next + 1) % this.size;
|
||||
- this.count = Math.min(this.size, this.count + 1);
|
||||
- return oldCount === this.size ? oldValue : null;
|
||||
- },
|
||||
-
|
||||
- front(): number | null {
|
||||
- const notEmpty = this.count > 0;
|
||||
- if (notEmpty) {
|
||||
- const current = this.next - 1;
|
||||
- const index = current < 0 ? this.size - 1 : current;
|
||||
- return this.buffer[index];
|
||||
- }
|
||||
- return null;
|
||||
- },
|
||||
-
|
||||
- back(): number | null {
|
||||
- const notEmpty = this.count > 0;
|
||||
- return notEmpty ? this.buffer[this.next] : null;
|
||||
- },
|
||||
- };
|
||||
-}
|
||||
-
|
||||
-const DEFAULT_BUFFER_SIZE = 20;
|
||||
-addWhitelistedNativeProps({ text: true });
|
||||
-const AnimatedTextInput = createAnimatedComponent(TextInput);
|
||||
-
|
||||
-function loopAnimationFrame(fn: (lastTime: number, time: number) => void) {
|
||||
- let lastTime = 0;
|
||||
-
|
||||
- function loop() {
|
||||
- requestAnimationFrame((time) => {
|
||||
- if (lastTime > 0) {
|
||||
- fn(lastTime, time);
|
||||
- }
|
||||
- lastTime = time;
|
||||
- requestAnimationFrame(loop);
|
||||
- });
|
||||
- }
|
||||
-
|
||||
- loop();
|
||||
-}
|
||||
-
|
||||
-function getFps(renderTimeInMs: number): number {
|
||||
- 'worklet';
|
||||
- return 1000 / renderTimeInMs;
|
||||
-}
|
||||
-
|
||||
-function completeBufferRoutine(
|
||||
- buffer: CircularBuffer,
|
||||
- timestamp: number
|
||||
-): number {
|
||||
- 'worklet';
|
||||
- timestamp = Math.round(timestamp);
|
||||
-
|
||||
- const droppedTimestamp = buffer.push(timestamp) ?? timestamp;
|
||||
-
|
||||
- const measuredRangeDuration = timestamp - droppedTimestamp;
|
||||
-
|
||||
- return getFps(measuredRangeDuration / buffer.count);
|
||||
-}
|
||||
-
|
||||
-function JsPerformance({ smoothingFrames }: { smoothingFrames: number }) {
|
||||
- const jsFps = useSharedValue<string | null>(null);
|
||||
- const totalRenderTime = useSharedValue(0);
|
||||
- const circularBuffer = useRef<CircularBuffer>(
|
||||
- createCircularDoublesBuffer(smoothingFrames)
|
||||
- );
|
||||
-
|
||||
- useEffect(() => {
|
||||
- loopAnimationFrame((_, timestamp) => {
|
||||
- timestamp = Math.round(timestamp);
|
||||
-
|
||||
- const currentFps = completeBufferRoutine(
|
||||
- circularBuffer.current,
|
||||
- timestamp
|
||||
- );
|
||||
-
|
||||
- // JS fps have to be measured every 2nd frame,
|
||||
- // thus 2x multiplication has to occur here
|
||||
- jsFps.value = (currentFps * 2).toFixed(0);
|
||||
- });
|
||||
- }, [jsFps, totalRenderTime]);
|
||||
-
|
||||
- const animatedProps = useAnimatedProps(() => {
|
||||
- const text = 'JS: ' + (jsFps.value ?? 'N/A') + ' ';
|
||||
- return { text, defaultValue: text };
|
||||
- });
|
||||
-
|
||||
- return (
|
||||
- <View style={styles.container}>
|
||||
- <AnimatedTextInput
|
||||
- style={styles.text}
|
||||
- animatedProps={animatedProps}
|
||||
- editable={false}
|
||||
- />
|
||||
- </View>
|
||||
- );
|
||||
-}
|
||||
-
|
||||
-function UiPerformance({ smoothingFrames }: { smoothingFrames: number }) {
|
||||
- const uiFps = useSharedValue<string | null>(null);
|
||||
- const circularBuffer = useSharedValue<CircularBuffer | null>(null);
|
||||
-
|
||||
- useFrameCallback(({ timestamp }: FrameInfo) => {
|
||||
- if (circularBuffer.value === null) {
|
||||
- circularBuffer.value = createCircularDoublesBuffer(smoothingFrames);
|
||||
- }
|
||||
-
|
||||
- timestamp = Math.round(timestamp);
|
||||
-
|
||||
- const currentFps = completeBufferRoutine(circularBuffer.value, timestamp);
|
||||
-
|
||||
- uiFps.value = currentFps.toFixed(0);
|
||||
- });
|
||||
-
|
||||
- const animatedProps = useAnimatedProps(() => {
|
||||
- const text = 'UI: ' + (uiFps.value ?? 'N/A') + ' ';
|
||||
- return { text, defaultValue: text };
|
||||
- });
|
||||
-
|
||||
- return (
|
||||
- <View style={styles.container}>
|
||||
- <AnimatedTextInput
|
||||
- style={styles.text}
|
||||
- animatedProps={animatedProps}
|
||||
- editable={false}
|
||||
- />
|
||||
- </View>
|
||||
- );
|
||||
-}
|
||||
-
|
||||
-export type PerformanceMonitorProps = {
|
||||
- /**
|
||||
- * Sets amount of previous frames used for smoothing at highest expectedFps.
|
||||
- *
|
||||
- * Automatically scales down at lower frame rates.
|
||||
- *
|
||||
- * Affects jumpiness of the FPS measurements value.
|
||||
- */
|
||||
- smoothingFrames?: number;
|
||||
-};
|
||||
-
|
||||
/**
|
||||
* A component that lets you measure fps values on JS and UI threads on both the
|
||||
* Paper and Fabric architectures.
|
||||
@@ -172,40 +7,6 @@ export type PerformanceMonitorProps = {
|
||||
* @param smoothingFrames - Determines amount of saved frames which will be used
|
||||
* for fps value smoothing.
|
||||
*/
|
||||
-export function PerformanceMonitor({
|
||||
- smoothingFrames = DEFAULT_BUFFER_SIZE,
|
||||
-}: PerformanceMonitorProps) {
|
||||
- return (
|
||||
- <View style={styles.monitor}>
|
||||
- <JsPerformance smoothingFrames={smoothingFrames} />
|
||||
- <UiPerformance smoothingFrames={smoothingFrames} />
|
||||
- </View>
|
||||
- );
|
||||
+export function PerformanceMonitor() {
|
||||
+ return null;
|
||||
}
|
||||
-
|
||||
-const styles = StyleSheet.create({
|
||||
- monitor: {
|
||||
- flexDirection: 'row',
|
||||
- position: 'absolute',
|
||||
- backgroundColor: '#0006',
|
||||
- zIndex: 1000,
|
||||
- },
|
||||
- header: {
|
||||
- fontSize: 14,
|
||||
- color: '#ffff',
|
||||
- paddingHorizontal: 5,
|
||||
- },
|
||||
- text: {
|
||||
- fontSize: 13,
|
||||
- fontVariant: ['tabular-nums'],
|
||||
- color: '#ffff',
|
||||
- fontFamily: 'monospace',
|
||||
- paddingHorizontal: 3,
|
||||
- },
|
||||
- container: {
|
||||
- alignItems: 'center',
|
||||
- justifyContent: 'center',
|
||||
- flexDirection: 'row',
|
||||
- flexWrap: 'wrap',
|
||||
- },
|
||||
-});
|
||||
@@ -0,0 +1,326 @@
|
||||
diff --git a/node_modules/react-native-screens/ios/RNSScreen.mm b/node_modules/react-native-screens/ios/RNSScreen.mm
|
||||
index b62a2e2..cb469db 100644
|
||||
--- a/node_modules/react-native-screens/ios/RNSScreen.mm
|
||||
+++ b/node_modules/react-native-screens/ios/RNSScreen.mm
|
||||
@@ -729,9 +729,26 @@ - (void)notifyTransitionProgress:(double)progress closing:(BOOL)closing goingFor
|
||||
#endif
|
||||
}
|
||||
|
||||
-#if !RCT_NEW_ARCH_ENABLED
|
||||
+- (void)willMoveToWindow:(UIWindow *)newWindow
|
||||
+{
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // In iOS 26, as soon as another screen appears in transition, it is interactable
|
||||
+ // To avoid glitches resulting from clicking buttons mid transition, we temporarily disable all interactions
|
||||
+ // Disabling interactions for parent navigation controller won't be enough in case of nested stack
|
||||
+ // Furthermore, a stack put inside a modal will exist in an entirely different hierarchy
|
||||
+ // To be sure, we block interactions on the whole window.
|
||||
+ // Note that newWindows is nil when moving from instead of moving to, and Obj-C handles nil correctly
|
||||
+ newWindow.userInteractionEnabled = false;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
- (void)presentationControllerWillDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // Disable interactions to disallow multiple modals dismissed at once; see willMoveToWindow
|
||||
+ presentationController.containerView.window.userInteractionEnabled = false;
|
||||
+ }
|
||||
+#if !RCT_NEW_ARCH_ENABLED
|
||||
// On Paper, we need to call both "cancel" and "reset" here because RN's gesture
|
||||
// recognizer does not handle the scenario when it gets cancelled by other top
|
||||
// level gesture recognizer. In this case by the modal dismiss gesture.
|
||||
@@ -744,8 +761,8 @@ - (void)presentationControllerWillDismiss:(UIPresentationController *)presentati
|
||||
// down.
|
||||
[_touchHandler cancel];
|
||||
[_touchHandler reset];
|
||||
-}
|
||||
#endif // !RCT_NEW_ARCH_ENABLED
|
||||
+}
|
||||
|
||||
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
@@ -757,6 +774,10 @@ - (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presenta
|
||||
|
||||
- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // Reenable interactions; see presentationControllerWillDismiss
|
||||
+ presentationController.containerView.window.userInteractionEnabled = true;
|
||||
+ }
|
||||
// NOTE(kkafar): We should consider depracating the use of gesture cancel here & align
|
||||
// with usePreventRemove API of react-navigation v7.
|
||||
[self notifyGestureCancel];
|
||||
@@ -767,6 +788,11 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)pr
|
||||
|
||||
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // Reenable interactions; see presentationControllerWillDismiss
|
||||
+ // Dismissed screen doesn't hold a reference to window, but presentingViewController.view does
|
||||
+ presentationController.presentingViewController.view.window.userInteractionEnabled = true;
|
||||
+ }
|
||||
if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
|
||||
[_reactSuperview performSelector:@selector(presentationControllerDidDismiss:) withObject:presentationController];
|
||||
}
|
||||
@@ -1518,6 +1544,10 @@ - (void)viewWillDisappear:(BOOL)animated
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // Reenable interactions, see willMoveToWindow
|
||||
+ self.view.window.userInteractionEnabled = true;
|
||||
+ }
|
||||
[super viewDidAppear:animated];
|
||||
if (!_isSwiping || _shouldNotify) {
|
||||
// we are going forward or dismissing without swipe
|
||||
diff --git a/node_modules/react-native-screens/ios/RNSScreenStack.mm b/node_modules/react-native-screens/ios/RNSScreenStack.mm
|
||||
index 229dc58..10b365b 100644
|
||||
--- a/node_modules/react-native-screens/ios/RNSScreenStack.mm
|
||||
+++ b/node_modules/react-native-screens/ios/RNSScreenStack.mm
|
||||
@@ -62,26 +62,6 @@ @interface RNSScreenStackView () <
|
||||
|
||||
@implementation RNSNavigationController
|
||||
|
||||
-#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
||||
-- (void)viewDidLoad
|
||||
-{
|
||||
- // iOS 26 introduces new gesture recognizer which replaces our RNSPanGestureRecognizer.
|
||||
- // The problem is that we are not able to handle it here for various reasons:
|
||||
- // - the new recognizer comes with its own delegate and our current approach is to wire
|
||||
- // all recognizers to RNSScreenStackView; to be 100% sure we don't break the logic,
|
||||
- // we would have to decorate its delegate and call it after our code, which would
|
||||
- // break other recognizers that the stack view is the delegate for
|
||||
- // - when RNSScreenStackView.setupGestureHandler method is called, the recognizer hasn't been
|
||||
- // loaded yet and there is no other place to configure in a not "hacky" way
|
||||
- // - the official docs warn us to not use it for anything other than "setting up failure requirements with it"
|
||||
- // - we expose fullScreenGestureEnabled prop to enable/disable the feature,
|
||||
- // so we need control over the delegate
|
||||
- if (@available(iOS 26.0, *)) {
|
||||
- self.interactiveContentPopGestureRecognizer.enabled = NO;
|
||||
- }
|
||||
-}
|
||||
-#endif // iOS 26
|
||||
-
|
||||
#if !TARGET_OS_TV
|
||||
- (UIViewController *)childViewControllerForStatusBarStyle
|
||||
{
|
||||
@@ -219,50 +199,6 @@ - (bool)onRepeatedTabSelectionOfTabScreenController:(RNSTabsScreenViewController
|
||||
return false;
|
||||
}
|
||||
|
||||
-#pragma mark - UINavigationBarDelegate
|
||||
-
|
||||
-#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
||||
-- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
|
||||
-{
|
||||
- if (@available(iOS 26, *)) {
|
||||
- // To prevent popping multiple screens when back button is pressed repeatedly,
|
||||
- // We allow for pop operation to proceed only if no transition is in progress,
|
||||
- // which we check indirectly by checking if transitionCoordinator is set.
|
||||
- // If it's not, we are safe to proceed.
|
||||
- if (self.transitionCoordinator == nil) {
|
||||
- // We still need to disable interactions for back button so click effects are not applied,
|
||||
- // and there is unfortunately no better place for it currently
|
||||
- UIView *button = [navigationBar rnscreens_findBackButtonWrapperView];
|
||||
- if (button != nil) {
|
||||
- button.userInteractionEnabled = false;
|
||||
- }
|
||||
-
|
||||
- return true;
|
||||
- }
|
||||
-
|
||||
- return false;
|
||||
- }
|
||||
-
|
||||
- return true;
|
||||
-}
|
||||
-
|
||||
-- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item
|
||||
-{
|
||||
- if (@available(iOS 26, *)) {
|
||||
- // Reset interactions on back button -> see navigationBar:shouldPopItem
|
||||
- // IMPORTANT: This reset won't execute when preventNativeDismiss is on.
|
||||
- // However, on iOS 26, unlike in previous versions, the back button instance changes
|
||||
- // when handling preventNativeDismiss and userIteractionEnabled is reset.
|
||||
- // The instance also changes when regular screen pop happens, but in that case
|
||||
- // the value of userInteractionEnabled is carried on, and we reset it here.
|
||||
- UIView *button = [navigationBar rnscreens_findBackButtonWrapperView];
|
||||
- if (button != nil) {
|
||||
- button.userInteractionEnabled = true;
|
||||
- }
|
||||
- }
|
||||
-}
|
||||
-#endif // Check for iOS >= 26
|
||||
-
|
||||
#pragma mark - RNSFrameCorrectionProvider
|
||||
|
||||
#ifdef RNS_GAMMA_ENABLED
|
||||
@@ -327,7 +263,7 @@ @implementation RNSScreenStackView {
|
||||
UINavigationController *_controller;
|
||||
NSMutableArray<RNSScreenView *> *_reactSubviews;
|
||||
BOOL _invalidated;
|
||||
- BOOL _isFullWidthSwiping;
|
||||
+ BOOL _isFullWidthSwipingWithPanGesture; // used only for content swipe with RNSPanGestureRecognizer
|
||||
RNSPercentDrivenInteractiveTransition *_interactionController;
|
||||
__weak RNSScreenStackManager *_manager;
|
||||
BOOL _updateScheduled;
|
||||
@@ -522,6 +458,11 @@ - (void)reactAddControllerToClosestParent:(UIViewController *)controller
|
||||
[self addSubview:controller.view];
|
||||
#if !TARGET_OS_TV
|
||||
_controller.interactivePopGestureRecognizer.delegate = self;
|
||||
+ #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ _controller.interactiveContentPopGestureRecognizer.delegate = self;
|
||||
+ }
|
||||
+#endif // Check for iOS >= 26.0
|
||||
#endif
|
||||
[controller didMoveToParentViewController:parentView.reactViewController];
|
||||
// On iOS pre 12 we observed that `willShowViewController` delegate method does not always
|
||||
@@ -943,7 +884,7 @@ - (void)dismissOnReload
|
||||
// when preventing the native dismiss with back button, we have to return the animator.
|
||||
// Also, we need to return the animator when full width swiping even if the animation is not custom,
|
||||
// otherwise the screen will be just popped immediately due to no animation
|
||||
- ((operation == UINavigationControllerOperationPop && shouldCancelDismiss) || _isFullWidthSwiping ||
|
||||
+ ((operation == UINavigationControllerOperationPop && shouldCancelDismiss) || _isFullWidthSwipingWithPanGesture ||
|
||||
[RNSScreenStackAnimator isCustomAnimation:screen.stackAnimation] || _customAnimation)) {
|
||||
return [[RNSScreenStackAnimator alloc] initWithOperation:operation];
|
||||
}
|
||||
@@ -967,23 +908,39 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
||||
}
|
||||
RNSScreenView *topScreen = _reactSubviews.lastObject;
|
||||
|
||||
+ BOOL customAnimationOnSwipePropSetAndSelectedAnimationIsCustom =
|
||||
+ topScreen.customAnimationOnSwipe && [RNSScreenStackAnimator isCustomAnimation:topScreen.stackAnimation];
|
||||
+
|
||||
#if TARGET_OS_TV || TARGET_OS_VISION
|
||||
[self cancelTouchesInParent];
|
||||
return YES;
|
||||
#else
|
||||
- // RNSPanGestureRecognizer will receive events iff topScreen.fullScreenSwipeEnabled == YES;
|
||||
- // Events are filtered in gestureRecognizer:shouldReceivePressOrTouchEvent: method
|
||||
if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) {
|
||||
- if ([self isInGestureResponseDistance:gestureRecognizer topScreen:topScreen]) {
|
||||
- _isFullWidthSwiping = YES;
|
||||
- [self cancelTouchesInParent];
|
||||
- return YES;
|
||||
+ // On iOS < 26, we have a custom full screen swipe recognizer that functions similarily
|
||||
+ // to interactiveContentPopGestureRecognizer introduced in iOS 26.
|
||||
+ // On iOS >= 26, we want to use the native one, but we are unable to handle custom animations
|
||||
+ // with native interactiveContentPopGestureRecognizer, so we have to fallback to the old implementation.
|
||||
+ // In this case, the old one should behave as close as the new native one, having only the difference
|
||||
+ // in animation, and without any customization that is exclusive for it (e.g. gestureResponseDistance).
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ if (customAnimationOnSwipePropSetAndSelectedAnimationIsCustom) {
|
||||
+ _isFullWidthSwipingWithPanGesture = YES;
|
||||
+ [self cancelTouchesInParent];
|
||||
+ return YES;
|
||||
+ }
|
||||
+ return NO;
|
||||
+ } else {
|
||||
+ if ([self isInGestureResponseDistance:gestureRecognizer topScreen:topScreen]) {
|
||||
+ _isFullWidthSwipingWithPanGesture = YES;
|
||||
+ [self cancelTouchesInParent];
|
||||
+ return YES;
|
||||
+ }
|
||||
+ return NO;
|
||||
}
|
||||
- return NO;
|
||||
}
|
||||
|
||||
// Now we're dealing with RNSScreenEdgeGestureRecognizer (or _UIParallaxTransitionPanGestureRecognizer)
|
||||
- if (topScreen.customAnimationOnSwipe && [RNSScreenStackAnimator isCustomAnimation:topScreen.stackAnimation]) {
|
||||
+ if (customAnimationOnSwipePropSetAndSelectedAnimationIsCustom) {
|
||||
if ([gestureRecognizer isKindOfClass:[RNSScreenEdgeGestureRecognizer class]]) {
|
||||
UIRectEdge edges = ((RNSScreenEdgeGestureRecognizer *)gestureRecognizer).edges;
|
||||
BOOL isRTL = _controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft;
|
||||
@@ -1028,7 +985,9 @@ - (void)setupGestureHandlers
|
||||
rightEdgeSwipeGestureRecognizer.delegate = self;
|
||||
[self addGestureRecognizer:rightEdgeSwipeGestureRecognizer];
|
||||
|
||||
- // gesture recognizer for full width swipe gesture
|
||||
+ // Starting from iOS 26, RNSPanGestureRecognizer has been mostly replaced by native
|
||||
+ // interactiveContentPopGestureRecognizer. It still needs to handle custom dismiss animations,
|
||||
+ // which we are not able to handle with the latter.
|
||||
RNSPanGestureRecognizer *panRecognizer = [[RNSPanGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(handleSwipe:)];
|
||||
panRecognizer.delegate = self;
|
||||
@@ -1091,7 +1050,7 @@ - (void)handleSwipe:(UIPanGestureRecognizer *)gestureRecognizer
|
||||
[_interactionController cancelInteractiveTransition];
|
||||
}
|
||||
_interactionController = nil;
|
||||
- _isFullWidthSwiping = NO;
|
||||
+ _isFullWidthSwipingWithPanGesture = NO;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
@@ -1225,14 +1184,6 @@ - (BOOL)isScrollViewPanGestureRecognizer:(UIGestureRecognizer *)gestureRecognize
|
||||
// Be careful when adding another type of gesture recognizer.
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePressOrTouchEvent:(NSObject *)event
|
||||
{
|
||||
- if (@available(iOS 26, *)) {
|
||||
- // in iOS 26, you can swipe to pop screen before the previous one finished transitioning;
|
||||
- // this prevents from registering the second gesture
|
||||
- if ([self isTransitionInProgress]) {
|
||||
- return NO;
|
||||
- }
|
||||
- }
|
||||
-
|
||||
RNSScreenView *topScreen = _reactSubviews.lastObject;
|
||||
|
||||
for (RNSScreenView *s in _reactSubviews.reverseObjectEnumerator) {
|
||||
@@ -1249,10 +1200,30 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ BOOL customAnimationOnSwipePropSetAndSelectedAnimationIsCustom =
|
||||
+ topScreen.customAnimationOnSwipe && [RNSScreenStackAnimator isCustomAnimation:topScreen.stackAnimation];
|
||||
+#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
|
||||
+ if (@available(iOS 26, *)) {
|
||||
+ // On iOS 26, fullScreenSwipeEnabled takes no effect, and depending on whether custom animations are on,
|
||||
+ // we select either interactiveContentPopGestureRecognizer or RNSPanGestureRecognizer
|
||||
+ if (([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]] &&
|
||||
+ !customAnimationOnSwipePropSetAndSelectedAnimationIsCustom) ||
|
||||
+ (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer &&
|
||||
+ customAnimationOnSwipePropSetAndSelectedAnimationIsCustom)) {
|
||||
+ return NO;
|
||||
+ }
|
||||
+ } else {
|
||||
+ // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled.
|
||||
+ if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) {
|
||||
+ return topScreen.fullScreenSwipeEnabled;
|
||||
+ }
|
||||
+ }
|
||||
+#else // check for iOS >= 26
|
||||
// We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled.
|
||||
if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) {
|
||||
return topScreen.fullScreenSwipeEnabled;
|
||||
}
|
||||
+#endif // check for iOS >= 26
|
||||
|
||||
// RNSScreenEdgeGestureRecognizer || _UIParallaxTransitionPanGestureRecognizer
|
||||
return YES;
|
||||
@@ -1268,15 +1239,6 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive
|
||||
return [self gestureRecognizer:gestureRecognizer shouldReceivePressOrTouchEvent:touch];
|
||||
}
|
||||
|
||||
-- (BOOL)isTransitionInProgress
|
||||
-{
|
||||
- if (_controller.transitionCoordinator != nil) {
|
||||
- return YES;
|
||||
- }
|
||||
-
|
||||
- return NO;
|
||||
-}
|
||||
-
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
||||
{
|
||||
@@ -1289,7 +1251,6 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateBegan || isBackGesture) {
|
||||
return NO;
|
||||
}
|
||||
-
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
@@ -0,0 +1,33 @@
|
||||
diff --git a/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift b/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
|
||||
index c34ba71..13d576a 100644
|
||||
--- a/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
|
||||
+++ b/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
|
||||
@@ -159,13 +159,23 @@ class RNUITextViewShadow: RCTShadowView {
|
||||
let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
|
||||
let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
|
||||
|
||||
- var totalLines = self.lineHeight == 0.0 ? 0 : Int(ceil(textSize.height / self.lineHeight))
|
||||
-
|
||||
- if self.numberOfLines != 0, totalLines > self.numberOfLines {
|
||||
- totalLines = self.numberOfLines
|
||||
+ var finalHeight: CGFloat
|
||||
+
|
||||
+ if self.numberOfLines != 0 && self.lineHeight != 0.0 {
|
||||
+ // numberOfLines is set with custom line height - need to calculate lines and snap to lineHeight multiples
|
||||
+ // NOTE: this calculation can be inaccurate with fractional font sizes
|
||||
+ var totalLines = Int(ceil(textSize.height / self.lineHeight))
|
||||
+ if totalLines > self.numberOfLines {
|
||||
+ totalLines = self.numberOfLines
|
||||
+ }
|
||||
+ finalHeight = CGFloat(totalLines) * self.lineHeight
|
||||
+ } else {
|
||||
+ // Either no numberOfLines limit, or no custom lineHeight - use actual text height
|
||||
+ // (numberOfLines without custom lineHeight is handled by the UITextView's textContainer.maximumNumberOfLines)
|
||||
+ finalHeight = textSize.height
|
||||
}
|
||||
|
||||
- self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight))
|
||||
+ self.frameSize = CGSize(width: CGFloat(maxWidth), height: finalHeight)
|
||||
return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
diff --git a/node_modules/sonner-native/lib/commonjs/toast.js b/node_modules/sonner-native/lib/commonjs/toast.js
|
||||
index 121816a..0c3c7bd 100644
|
||||
--- a/node_modules/sonner-native/lib/commonjs/toast.js
|
||||
+++ b/node_modules/sonner-native/lib/commonjs/toast.js
|
||||
@@ -264,7 +264,7 @@ const Toast = exports.Toast = /*#__PURE__*/React.forwardRef(({
|
||||
...toastSwipeHandlerProps,
|
||||
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
||||
entering: entering,
|
||||
- exiting: exiting,
|
||||
+ exiting: _reactNative.Platform.OS === 'android' ? undefined : exiting,
|
||||
children: jsx
|
||||
})
|
||||
});
|
||||
@@ -274,7 +274,7 @@ const Toast = exports.Toast = /*#__PURE__*/React.forwardRef(({
|
||||
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
||||
style: [unstyled ? undefined : elevationStyle, defaultStyles.toast, toastStyleCtx, styles?.toast, style, wiggleAnimationStyle],
|
||||
entering: entering,
|
||||
- exiting: exiting,
|
||||
+ exiting: _reactNative.Platform.OS === 'android' ? undefined : exiting,
|
||||
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
||||
style: [defaultStyles.toastContent, toastContentStyleCtx, styles?.toastContent],
|
||||
children: [promiseOptions || variant === 'loading' ? 'loading' in icons ? icons.loading : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {}) : icon ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
||||
@@ -0,0 +1,3 @@
|
||||
# sonner-native+0.21.0.patch
|
||||
|
||||
Removes Reanimated exit layout animations from the toasts. This was causing crashes if the toast was hidden while you were scrolling a flatlist.
|
||||
@@ -4,7 +4,6 @@ import '#/view/icons'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import {
|
||||
initialWindowMetrics,
|
||||
SafeAreaProvider,
|
||||
@@ -84,7 +83,11 @@ if (isIOS) {
|
||||
}
|
||||
if (isAndroid) {
|
||||
// iOS is handled by the config plugin -sfn
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP)
|
||||
ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||
).catch(error =>
|
||||
logger.debug('Could not lock orientation', {safeMessage: error}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,64 +136,62 @@ function InnerApp() {
|
||||
<ThemeProvider theme={theme}>
|
||||
<ContextMenuProvider>
|
||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||
<RootSiblingParent>
|
||||
<VideoVolumeProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<GlobalGestureEventsProvider>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</GlobalGestureEventsProvider>
|
||||
</GestureHandlerRootView>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</VideoVolumeProvider>
|
||||
</RootSiblingParent>
|
||||
<VideoVolumeProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<GlobalGestureEventsProvider>
|
||||
<IntentDialogProvider>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</GlobalGestureEventsProvider>
|
||||
</GestureHandlerRootView>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</VideoVolumeProvider>
|
||||
</Splash>
|
||||
</ContextMenuProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -3,7 +3,6 @@ import '#/view/icons'
|
||||
import './style.css'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -111,62 +110,60 @@ function InnerApp() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ContextMenuProvider>
|
||||
<RootSiblingParent>
|
||||
<VideoVolumeProvider>
|
||||
<ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</ActiveVideoProvider>
|
||||
</VideoVolumeProvider>
|
||||
</RootSiblingParent>
|
||||
<VideoVolumeProvider>
|
||||
<ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<PolicyUpdateOverlayProvider>
|
||||
<StatsigProvider>
|
||||
<AgeAssuranceProvider>
|
||||
<ComposerProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<ToastOutlet />
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</EmailVerificationProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</ComposerProvider>
|
||||
</AgeAssuranceProvider>
|
||||
</StatsigProvider>
|
||||
</PolicyUpdateOverlayProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</ActiveVideoProvider>
|
||||
</VideoVolumeProvider>
|
||||
</ContextMenuProvider>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
|
||||
@@ -7,11 +7,11 @@ const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe
|
||||
|
||||
const factor = 0.0625 // 1 - (15/16)
|
||||
const fontScaleMultipliers: Record<Device['fontScale'], number> = {
|
||||
'-2': 1 - factor * 3,
|
||||
'-1': 1 - factor * 2,
|
||||
'0': 1 - factor * 1, // default
|
||||
'1': 1,
|
||||
'2': 1 + factor * 1,
|
||||
'-2': 1 - factor * 1, // unused
|
||||
'-1': 1 - factor * 1,
|
||||
'0': 1, // default
|
||||
'1': 1 + factor * 1,
|
||||
'2': 1 + factor * 1, // unused
|
||||
}
|
||||
|
||||
export function computeFontScaleMultiplier(scale: Device['fontScale']) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import {type Theme, type ThemeName} from '@bsky.app/alf'
|
||||
|
||||
import {
|
||||
computeFontScaleMultiplier,
|
||||
@@ -7,16 +8,14 @@ import {
|
||||
setFontFamily as persistFontFamily,
|
||||
setFontScale as persistFontScale,
|
||||
} from '#/alf/fonts'
|
||||
import {createThemes, defaultTheme} from '#/alf/themes'
|
||||
import {type Theme, type ThemeName} from '#/alf/types'
|
||||
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||
import {themes} from '#/alf/themes'
|
||||
import {type Device} from '#/storage'
|
||||
|
||||
export {type TextStyleProp, type Theme, type ViewStyleProp} from '@bsky.app/alf'
|
||||
export {atoms} from '#/alf/atoms'
|
||||
export * from '#/alf/breakpoints'
|
||||
export * from '#/alf/fonts'
|
||||
export * as tokens from '#/alf/tokens'
|
||||
export * from '#/alf/types'
|
||||
export * from '#/alf/util/flatten'
|
||||
export * from '#/alf/util/platform'
|
||||
export * from '#/alf/util/themeSelector'
|
||||
@@ -25,7 +24,7 @@ export * from '#/alf/util/useGutters'
|
||||
export type Alf = {
|
||||
themeName: ThemeName
|
||||
theme: Theme
|
||||
themes: ReturnType<typeof createThemes>
|
||||
themes: typeof themes
|
||||
fonts: {
|
||||
scale: Exclude<Device['fontScale'], undefined>
|
||||
scaleMultiplier: number
|
||||
@@ -44,14 +43,8 @@ export type Alf = {
|
||||
*/
|
||||
export const Context = React.createContext<Alf>({
|
||||
themeName: 'light',
|
||||
theme: defaultTheme,
|
||||
themes: createThemes({
|
||||
hues: {
|
||||
primary: BLUE_HUE,
|
||||
negative: RED_HUE,
|
||||
positive: GREEN_HUE,
|
||||
},
|
||||
}),
|
||||
theme: themes.light,
|
||||
themes,
|
||||
fonts: {
|
||||
scale: getFontScale(),
|
||||
scaleMultiplier: computeFontScaleMultiplier(getFontScale()),
|
||||
@@ -76,10 +69,10 @@ export function ThemeProvider({
|
||||
const setFontScaleAndPersist = React.useCallback<
|
||||
Alf['fonts']['setFontScale']
|
||||
>(
|
||||
fontScale => {
|
||||
setFontScale(fontScale)
|
||||
persistFontScale(fontScale)
|
||||
setFontScaleMultiplier(computeFontScaleMultiplier(fontScale))
|
||||
fs => {
|
||||
setFontScale(fs)
|
||||
persistFontScale(fs)
|
||||
setFontScaleMultiplier(computeFontScaleMultiplier(fs))
|
||||
},
|
||||
[setFontScale],
|
||||
)
|
||||
@@ -89,21 +82,12 @@ export function ThemeProvider({
|
||||
const setFontFamilyAndPersist = React.useCallback<
|
||||
Alf['fonts']['setFontFamily']
|
||||
>(
|
||||
fontFamily => {
|
||||
setFontFamily(fontFamily)
|
||||
persistFontFamily(fontFamily)
|
||||
ff => {
|
||||
setFontFamily(ff)
|
||||
persistFontFamily(ff)
|
||||
},
|
||||
[setFontFamily],
|
||||
)
|
||||
const themes = React.useMemo(() => {
|
||||
return createThemes({
|
||||
hues: {
|
||||
primary: BLUE_HUE,
|
||||
negative: RED_HUE,
|
||||
positive: GREEN_HUE,
|
||||
},
|
||||
})
|
||||
}, [])
|
||||
|
||||
const value = React.useMemo<Alf>(
|
||||
() => ({
|
||||
@@ -121,7 +105,6 @@ export function ThemeProvider({
|
||||
}),
|
||||
[
|
||||
themeName,
|
||||
themes,
|
||||
fontScale,
|
||||
setFontScaleAndPersist,
|
||||
fontFamily,
|
||||
|
||||
@@ -1,585 +1,44 @@
|
||||
import {atoms} from '#/alf/atoms'
|
||||
import {type Palette, type Theme} from '#/alf/types'
|
||||
import {
|
||||
BLUE_HUE,
|
||||
defaultScale,
|
||||
dimScale,
|
||||
GREEN_HUE,
|
||||
RED_HUE,
|
||||
} from '#/alf/util/colorGeneration'
|
||||
createThemes,
|
||||
DEFAULT_PALETTE,
|
||||
DEFAULT_SUBDUED_PALETTE,
|
||||
} from '@bsky.app/alf'
|
||||
|
||||
const themes = createThemes({
|
||||
hues: {
|
||||
primary: BLUE_HUE,
|
||||
negative: RED_HUE,
|
||||
positive: GREEN_HUE,
|
||||
},
|
||||
const DEFAULT_THEMES = createThemes({
|
||||
defaultPalette: DEFAULT_PALETTE,
|
||||
subduedPalette: DEFAULT_SUBDUED_PALETTE,
|
||||
})
|
||||
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const lightPalette = themes.lightPalette
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const darkPalette = themes.darkPalette
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const dimPalette = themes.dimPalette
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const light = themes.light
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const dark = themes.dark
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const dim = themes.dim
|
||||
|
||||
export const defaultTheme = themes.light
|
||||
|
||||
export function createThemes({
|
||||
hues,
|
||||
}: {
|
||||
hues: {
|
||||
primary: number
|
||||
negative: number
|
||||
positive: number
|
||||
}
|
||||
}): {
|
||||
lightPalette: Palette
|
||||
darkPalette: Palette
|
||||
dimPalette: Palette
|
||||
light: Theme
|
||||
dark: Theme
|
||||
dim: Theme
|
||||
} {
|
||||
const color = {
|
||||
like: '#ec4899',
|
||||
trueBlack: '#000000',
|
||||
|
||||
gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`,
|
||||
gray_25: `hsl(${hues.primary}, 20%, ${defaultScale[13]}%)`,
|
||||
gray_50: `hsl(${hues.primary}, 20%, ${defaultScale[12]}%)`,
|
||||
gray_100: `hsl(${hues.primary}, 20%, ${defaultScale[11]}%)`,
|
||||
gray_200: `hsl(${hues.primary}, 20%, ${defaultScale[10]}%)`,
|
||||
gray_300: `hsl(${hues.primary}, 20%, ${defaultScale[9]}%)`,
|
||||
gray_400: `hsl(${hues.primary}, 20%, ${defaultScale[8]}%)`,
|
||||
gray_500: `hsl(${hues.primary}, 20%, ${defaultScale[7]}%)`,
|
||||
gray_600: `hsl(${hues.primary}, 24%, ${defaultScale[6]}%)`,
|
||||
gray_700: `hsl(${hues.primary}, 24%, ${defaultScale[5]}%)`,
|
||||
gray_800: `hsl(${hues.primary}, 28%, ${defaultScale[4]}%)`,
|
||||
gray_900: `hsl(${hues.primary}, 28%, ${defaultScale[3]}%)`,
|
||||
gray_950: `hsl(${hues.primary}, 28%, ${defaultScale[2]}%)`,
|
||||
gray_975: `hsl(${hues.primary}, 28%, ${defaultScale[1]}%)`,
|
||||
gray_1000: `hsl(${hues.primary}, 28%, ${defaultScale[0]}%)`,
|
||||
|
||||
primary_25: `hsl(${hues.primary}, 99%, 97%)`,
|
||||
primary_50: `hsl(${hues.primary}, 99%, 95%)`,
|
||||
primary_100: `hsl(${hues.primary}, 99%, 90%)`,
|
||||
primary_200: `hsl(${hues.primary}, 99%, 80%)`,
|
||||
primary_300: `hsl(${hues.primary}, 99%, 70%)`,
|
||||
primary_400: `hsl(${hues.primary}, 99%, 60%)`,
|
||||
primary_500: `hsl(${hues.primary}, 99%, 53%)`,
|
||||
primary_600: `hsl(${hues.primary}, 99%, 42%)`,
|
||||
primary_700: `hsl(${hues.primary}, 99%, 34%)`,
|
||||
primary_800: `hsl(${hues.primary}, 99%, 26%)`,
|
||||
primary_900: `hsl(${hues.primary}, 99%, 18%)`,
|
||||
primary_950: `hsl(${hues.primary}, 99%, 10%)`,
|
||||
primary_975: `hsl(${hues.primary}, 99%, 7%)`,
|
||||
|
||||
green_25: `hsl(${hues.positive}, 82%, 97%)`,
|
||||
green_50: `hsl(${hues.positive}, 82%, 95%)`,
|
||||
green_100: `hsl(${hues.positive}, 82%, 90%)`,
|
||||
green_200: `hsl(${hues.positive}, 82%, 80%)`,
|
||||
green_300: `hsl(${hues.positive}, 82%, 70%)`,
|
||||
green_400: `hsl(${hues.positive}, 82%, 60%)`,
|
||||
green_500: `hsl(${hues.positive}, 82%, 50%)`,
|
||||
green_600: `hsl(${hues.positive}, 82%, 42%)`,
|
||||
green_700: `hsl(${hues.positive}, 82%, 34%)`,
|
||||
green_800: `hsl(${hues.positive}, 82%, 26%)`,
|
||||
green_900: `hsl(${hues.positive}, 82%, 18%)`,
|
||||
green_950: `hsl(${hues.positive}, 82%, 10%)`,
|
||||
green_975: `hsl(${hues.positive}, 82%, 7%)`,
|
||||
|
||||
red_25: `hsl(${hues.negative}, 91%, 97%)`,
|
||||
red_50: `hsl(${hues.negative}, 91%, 95%)`,
|
||||
red_100: `hsl(${hues.negative}, 91%, 90%)`,
|
||||
red_200: `hsl(${hues.negative}, 91%, 80%)`,
|
||||
red_300: `hsl(${hues.negative}, 91%, 70%)`,
|
||||
red_400: `hsl(${hues.negative}, 91%, 60%)`,
|
||||
red_500: `hsl(${hues.negative}, 91%, 50%)`,
|
||||
red_600: `hsl(${hues.negative}, 91%, 42%)`,
|
||||
red_700: `hsl(${hues.negative}, 91%, 34%)`,
|
||||
red_800: `hsl(${hues.negative}, 91%, 26%)`,
|
||||
red_900: `hsl(${hues.negative}, 91%, 18%)`,
|
||||
red_950: `hsl(${hues.negative}, 91%, 10%)`,
|
||||
red_975: `hsl(${hues.negative}, 91%, 7%)`,
|
||||
} as const
|
||||
|
||||
const lightPalette = {
|
||||
white: color.gray_0,
|
||||
black: color.gray_1000,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: color.gray_25,
|
||||
contrast_50: color.gray_50,
|
||||
contrast_100: color.gray_100,
|
||||
contrast_200: color.gray_200,
|
||||
contrast_300: color.gray_300,
|
||||
contrast_400: color.gray_400,
|
||||
contrast_500: color.gray_500,
|
||||
contrast_600: color.gray_600,
|
||||
contrast_700: color.gray_700,
|
||||
contrast_800: color.gray_800,
|
||||
contrast_900: color.gray_900,
|
||||
contrast_950: color.gray_950,
|
||||
contrast_975: color.gray_975,
|
||||
|
||||
primary_25: color.primary_25,
|
||||
primary_50: color.primary_50,
|
||||
primary_100: color.primary_100,
|
||||
primary_200: color.primary_200,
|
||||
primary_300: color.primary_300,
|
||||
primary_400: color.primary_400,
|
||||
primary_500: color.primary_500,
|
||||
primary_600: color.primary_600,
|
||||
primary_700: color.primary_700,
|
||||
primary_800: color.primary_800,
|
||||
primary_900: color.primary_900,
|
||||
primary_950: color.primary_950,
|
||||
primary_975: color.primary_975,
|
||||
|
||||
positive_25: color.green_25,
|
||||
positive_50: color.green_50,
|
||||
positive_100: color.green_100,
|
||||
positive_200: color.green_200,
|
||||
positive_300: color.green_300,
|
||||
positive_400: color.green_400,
|
||||
positive_500: color.green_500,
|
||||
positive_600: color.green_600,
|
||||
positive_700: color.green_700,
|
||||
positive_800: color.green_800,
|
||||
positive_900: color.green_900,
|
||||
positive_950: color.green_950,
|
||||
positive_975: color.green_975,
|
||||
|
||||
negative_25: color.red_25,
|
||||
negative_50: color.red_50,
|
||||
negative_100: color.red_100,
|
||||
negative_200: color.red_200,
|
||||
negative_300: color.red_300,
|
||||
negative_400: color.red_400,
|
||||
negative_500: color.red_500,
|
||||
negative_600: color.red_600,
|
||||
negative_700: color.red_700,
|
||||
negative_800: color.red_800,
|
||||
negative_900: color.red_900,
|
||||
negative_950: color.red_950,
|
||||
negative_975: color.red_975,
|
||||
} as const
|
||||
|
||||
const darkPalette: Palette = {
|
||||
white: color.gray_25,
|
||||
black: color.trueBlack,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: color.gray_975,
|
||||
contrast_50: color.gray_950,
|
||||
contrast_100: color.gray_900,
|
||||
contrast_200: color.gray_800,
|
||||
contrast_300: color.gray_700,
|
||||
contrast_400: color.gray_600,
|
||||
contrast_500: color.gray_500,
|
||||
contrast_600: color.gray_400,
|
||||
contrast_700: color.gray_300,
|
||||
contrast_800: color.gray_200,
|
||||
contrast_900: color.gray_100,
|
||||
contrast_950: color.gray_50,
|
||||
contrast_975: color.gray_25,
|
||||
|
||||
primary_25: color.primary_975,
|
||||
primary_50: color.primary_950,
|
||||
primary_100: color.primary_900,
|
||||
primary_200: color.primary_800,
|
||||
primary_300: color.primary_700,
|
||||
primary_400: color.primary_600,
|
||||
primary_500: color.primary_500,
|
||||
primary_600: color.primary_400,
|
||||
primary_700: color.primary_300,
|
||||
primary_800: color.primary_200,
|
||||
primary_900: color.primary_100,
|
||||
primary_950: color.primary_50,
|
||||
primary_975: color.primary_25,
|
||||
|
||||
positive_25: color.green_975,
|
||||
positive_50: color.green_950,
|
||||
positive_100: color.green_900,
|
||||
positive_200: color.green_800,
|
||||
positive_300: color.green_700,
|
||||
positive_400: color.green_600,
|
||||
positive_500: color.green_500,
|
||||
positive_600: color.green_400,
|
||||
positive_700: color.green_300,
|
||||
positive_800: color.green_200,
|
||||
positive_900: color.green_100,
|
||||
positive_950: color.green_50,
|
||||
positive_975: color.green_25,
|
||||
|
||||
negative_25: color.red_975,
|
||||
negative_50: color.red_950,
|
||||
negative_100: color.red_900,
|
||||
negative_200: color.red_800,
|
||||
negative_300: color.red_700,
|
||||
negative_400: color.red_600,
|
||||
negative_500: color.red_500,
|
||||
negative_600: color.red_400,
|
||||
negative_700: color.red_300,
|
||||
negative_800: color.red_200,
|
||||
negative_900: color.red_100,
|
||||
negative_950: color.red_50,
|
||||
negative_975: color.red_25,
|
||||
} as const
|
||||
|
||||
const dimPalette: Palette = {
|
||||
...darkPalette,
|
||||
black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`,
|
||||
contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`,
|
||||
contrast_100: `hsl(${hues.primary}, 28%, ${dimScale[3]}%)`,
|
||||
contrast_200: `hsl(${hues.primary}, 28%, ${dimScale[4]}%)`,
|
||||
contrast_300: `hsl(${hues.primary}, 24%, ${dimScale[5]}%)`,
|
||||
contrast_400: `hsl(${hues.primary}, 24%, ${dimScale[6]}%)`,
|
||||
contrast_500: `hsl(${hues.primary}, 20%, ${dimScale[7]}%)`,
|
||||
contrast_600: `hsl(${hues.primary}, 20%, ${dimScale[8]}%)`,
|
||||
contrast_700: `hsl(${hues.primary}, 20%, ${dimScale[9]}%)`,
|
||||
contrast_800: `hsl(${hues.primary}, 20%, ${dimScale[10]}%)`,
|
||||
contrast_900: `hsl(${hues.primary}, 20%, ${dimScale[11]}%)`,
|
||||
contrast_950: `hsl(${hues.primary}, 20%, ${dimScale[12]}%)`,
|
||||
contrast_975: `hsl(${hues.primary}, 20%, ${dimScale[13]}%)`,
|
||||
|
||||
primary_25: `hsl(${hues.primary}, 50%, ${dimScale[1]}%)`,
|
||||
primary_50: `hsl(${hues.primary}, 60%, ${dimScale[2]}%)`,
|
||||
primary_100: `hsl(${hues.primary}, 70%, ${dimScale[3]}%)`,
|
||||
primary_200: `hsl(${hues.primary}, 82%, ${dimScale[4]}%)`,
|
||||
primary_300: `hsl(${hues.primary}, 90%, ${dimScale[5]}%)`,
|
||||
primary_400: `hsl(${hues.primary}, 95%, ${dimScale[6]}%)`,
|
||||
primary_500: `hsl(${hues.primary}, 99%, ${dimScale[7]}%)`,
|
||||
primary_600: `hsl(${hues.primary}, 99%, ${dimScale[8]}%)`,
|
||||
primary_700: `hsl(${hues.primary}, 99%, ${dimScale[9]}%)`,
|
||||
primary_800: `hsl(${hues.primary}, 99%, ${dimScale[10]}%)`,
|
||||
primary_900: `hsl(${hues.primary}, 99%, ${dimScale[11]}%)`,
|
||||
primary_950: `hsl(${hues.primary}, 99%, ${dimScale[12]}%)`,
|
||||
primary_975: `hsl(${hues.primary}, 99%, ${dimScale[13]}%)`,
|
||||
|
||||
positive_25: `hsl(${hues.positive}, 50%, ${dimScale[1]}%)`,
|
||||
positive_50: `hsl(${hues.positive}, 60%, ${dimScale[2]}%)`,
|
||||
positive_100: `hsl(${hues.positive}, 70%, ${dimScale[3]}%)`,
|
||||
positive_200: `hsl(${hues.positive}, 82%, ${dimScale[4]}%)`,
|
||||
positive_300: `hsl(${hues.positive}, 82%, ${dimScale[5]}%)`,
|
||||
positive_400: `hsl(${hues.positive}, 82%, ${dimScale[6]}%)`,
|
||||
positive_500: `hsl(${hues.positive}, 82%, ${dimScale[7]}%)`,
|
||||
positive_600: `hsl(${hues.positive}, 82%, ${dimScale[8]}%)`,
|
||||
positive_700: `hsl(${hues.positive}, 82%, ${dimScale[9]}%)`,
|
||||
positive_800: `hsl(${hues.positive}, 82%, ${dimScale[10]}%)`,
|
||||
positive_900: `hsl(${hues.positive}, 82%, ${dimScale[11]}%)`,
|
||||
positive_950: `hsl(${hues.positive}, 82%, ${dimScale[12]}%)`,
|
||||
positive_975: `hsl(${hues.positive}, 82%, ${dimScale[13]}%)`,
|
||||
|
||||
negative_25: `hsl(${hues.negative}, 70%, ${dimScale[1]}%)`,
|
||||
negative_50: `hsl(${hues.negative}, 80%, ${dimScale[2]}%)`,
|
||||
negative_100: `hsl(${hues.negative}, 84%, ${dimScale[3]}%)`,
|
||||
negative_200: `hsl(${hues.negative}, 88%, ${dimScale[4]}%)`,
|
||||
negative_300: `hsl(${hues.negative}, 91%, ${dimScale[5]}%)`,
|
||||
negative_400: `hsl(${hues.negative}, 91%, ${dimScale[6]}%)`,
|
||||
negative_500: `hsl(${hues.negative}, 91%, ${dimScale[7]}%)`,
|
||||
negative_600: `hsl(${hues.negative}, 91%, ${dimScale[8]}%)`,
|
||||
negative_700: `hsl(${hues.negative}, 91%, ${dimScale[9]}%)`,
|
||||
negative_800: `hsl(${hues.negative}, 91%, ${dimScale[10]}%)`,
|
||||
negative_900: `hsl(${hues.negative}, 91%, ${dimScale[11]}%)`,
|
||||
negative_950: `hsl(${hues.negative}, 91%, ${dimScale[12]}%)`,
|
||||
negative_975: `hsl(${hues.negative}, 91%, ${dimScale[13]}%)`,
|
||||
} as const
|
||||
|
||||
const light: Theme = {
|
||||
scheme: 'light',
|
||||
name: 'light',
|
||||
palette: lightPalette,
|
||||
atoms: {
|
||||
text: {
|
||||
color: lightPalette.black,
|
||||
},
|
||||
text_contrast_low: {
|
||||
color: lightPalette.contrast_400,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: lightPalette.contrast_700,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: lightPalette.contrast_900,
|
||||
},
|
||||
text_inverted: {
|
||||
color: lightPalette.white,
|
||||
},
|
||||
bg: {
|
||||
backgroundColor: lightPalette.white,
|
||||
},
|
||||
bg_contrast_25: {
|
||||
backgroundColor: lightPalette.contrast_25,
|
||||
},
|
||||
bg_contrast_50: {
|
||||
backgroundColor: lightPalette.contrast_50,
|
||||
},
|
||||
bg_contrast_100: {
|
||||
backgroundColor: lightPalette.contrast_100,
|
||||
},
|
||||
bg_contrast_200: {
|
||||
backgroundColor: lightPalette.contrast_200,
|
||||
},
|
||||
bg_contrast_300: {
|
||||
backgroundColor: lightPalette.contrast_300,
|
||||
},
|
||||
bg_contrast_400: {
|
||||
backgroundColor: lightPalette.contrast_400,
|
||||
},
|
||||
bg_contrast_500: {
|
||||
backgroundColor: lightPalette.contrast_500,
|
||||
},
|
||||
bg_contrast_600: {
|
||||
backgroundColor: lightPalette.contrast_600,
|
||||
},
|
||||
bg_contrast_700: {
|
||||
backgroundColor: lightPalette.contrast_700,
|
||||
},
|
||||
bg_contrast_800: {
|
||||
backgroundColor: lightPalette.contrast_800,
|
||||
},
|
||||
bg_contrast_900: {
|
||||
backgroundColor: lightPalette.contrast_900,
|
||||
},
|
||||
bg_contrast_950: {
|
||||
backgroundColor: lightPalette.contrast_950,
|
||||
},
|
||||
bg_contrast_975: {
|
||||
backgroundColor: lightPalette.contrast_975,
|
||||
},
|
||||
border_contrast_low: {
|
||||
borderColor: lightPalette.contrast_100,
|
||||
},
|
||||
border_contrast_medium: {
|
||||
borderColor: lightPalette.contrast_200,
|
||||
},
|
||||
border_contrast_high: {
|
||||
borderColor: lightPalette.contrast_300,
|
||||
},
|
||||
shadow_sm: {
|
||||
...atoms.shadow_sm,
|
||||
shadowColor: lightPalette.black,
|
||||
},
|
||||
shadow_md: {
|
||||
...atoms.shadow_md,
|
||||
shadowColor: lightPalette.black,
|
||||
},
|
||||
shadow_lg: {
|
||||
...atoms.shadow_lg,
|
||||
shadowColor: lightPalette.black,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const dark: Theme = {
|
||||
scheme: 'dark',
|
||||
name: 'dark',
|
||||
palette: darkPalette,
|
||||
atoms: {
|
||||
text: {
|
||||
color: darkPalette.white,
|
||||
},
|
||||
text_contrast_low: {
|
||||
color: darkPalette.contrast_400,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: darkPalette.contrast_600,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: darkPalette.contrast_900,
|
||||
},
|
||||
text_inverted: {
|
||||
color: darkPalette.black,
|
||||
},
|
||||
bg: {
|
||||
backgroundColor: darkPalette.black,
|
||||
},
|
||||
bg_contrast_25: {
|
||||
backgroundColor: darkPalette.contrast_25,
|
||||
},
|
||||
bg_contrast_50: {
|
||||
backgroundColor: darkPalette.contrast_50,
|
||||
},
|
||||
bg_contrast_100: {
|
||||
backgroundColor: darkPalette.contrast_100,
|
||||
},
|
||||
bg_contrast_200: {
|
||||
backgroundColor: darkPalette.contrast_200,
|
||||
},
|
||||
bg_contrast_300: {
|
||||
backgroundColor: darkPalette.contrast_300,
|
||||
},
|
||||
bg_contrast_400: {
|
||||
backgroundColor: darkPalette.contrast_400,
|
||||
},
|
||||
bg_contrast_500: {
|
||||
backgroundColor: darkPalette.contrast_500,
|
||||
},
|
||||
bg_contrast_600: {
|
||||
backgroundColor: darkPalette.contrast_600,
|
||||
},
|
||||
bg_contrast_700: {
|
||||
backgroundColor: darkPalette.contrast_700,
|
||||
},
|
||||
bg_contrast_800: {
|
||||
backgroundColor: darkPalette.contrast_800,
|
||||
},
|
||||
bg_contrast_900: {
|
||||
backgroundColor: darkPalette.contrast_900,
|
||||
},
|
||||
bg_contrast_950: {
|
||||
backgroundColor: darkPalette.contrast_950,
|
||||
},
|
||||
bg_contrast_975: {
|
||||
backgroundColor: darkPalette.contrast_975,
|
||||
},
|
||||
border_contrast_low: {
|
||||
borderColor: darkPalette.contrast_100,
|
||||
},
|
||||
border_contrast_medium: {
|
||||
borderColor: darkPalette.contrast_200,
|
||||
},
|
||||
border_contrast_high: {
|
||||
borderColor: darkPalette.contrast_300,
|
||||
},
|
||||
shadow_sm: {
|
||||
...atoms.shadow_sm,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: color.trueBlack,
|
||||
},
|
||||
shadow_md: {
|
||||
...atoms.shadow_md,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: color.trueBlack,
|
||||
},
|
||||
shadow_lg: {
|
||||
...atoms.shadow_lg,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: color.trueBlack,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const dim: Theme = {
|
||||
...dark,
|
||||
scheme: 'dark',
|
||||
name: 'dim',
|
||||
palette: dimPalette,
|
||||
atoms: {
|
||||
...dark.atoms,
|
||||
text: {
|
||||
color: dimPalette.white,
|
||||
},
|
||||
text_contrast_low: {
|
||||
color: dimPalette.contrast_400,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: dimPalette.contrast_600,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: dimPalette.contrast_900,
|
||||
},
|
||||
text_inverted: {
|
||||
color: dimPalette.black,
|
||||
},
|
||||
bg: {
|
||||
backgroundColor: dimPalette.black,
|
||||
},
|
||||
bg_contrast_25: {
|
||||
backgroundColor: dimPalette.contrast_25,
|
||||
},
|
||||
bg_contrast_50: {
|
||||
backgroundColor: dimPalette.contrast_50,
|
||||
},
|
||||
bg_contrast_100: {
|
||||
backgroundColor: dimPalette.contrast_100,
|
||||
},
|
||||
bg_contrast_200: {
|
||||
backgroundColor: dimPalette.contrast_200,
|
||||
},
|
||||
bg_contrast_300: {
|
||||
backgroundColor: dimPalette.contrast_300,
|
||||
},
|
||||
bg_contrast_400: {
|
||||
backgroundColor: dimPalette.contrast_400,
|
||||
},
|
||||
bg_contrast_500: {
|
||||
backgroundColor: dimPalette.contrast_500,
|
||||
},
|
||||
bg_contrast_600: {
|
||||
backgroundColor: dimPalette.contrast_600,
|
||||
},
|
||||
bg_contrast_700: {
|
||||
backgroundColor: dimPalette.contrast_700,
|
||||
},
|
||||
bg_contrast_800: {
|
||||
backgroundColor: dimPalette.contrast_800,
|
||||
},
|
||||
bg_contrast_900: {
|
||||
backgroundColor: dimPalette.contrast_900,
|
||||
},
|
||||
bg_contrast_950: {
|
||||
backgroundColor: dimPalette.contrast_950,
|
||||
},
|
||||
bg_contrast_975: {
|
||||
backgroundColor: dimPalette.contrast_975,
|
||||
},
|
||||
border_contrast_low: {
|
||||
borderColor: dimPalette.contrast_100,
|
||||
},
|
||||
border_contrast_medium: {
|
||||
borderColor: dimPalette.contrast_200,
|
||||
},
|
||||
border_contrast_high: {
|
||||
borderColor: dimPalette.contrast_300,
|
||||
},
|
||||
shadow_sm: {
|
||||
...atoms.shadow_sm,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(${hues.primary}, 28%, 6%)`,
|
||||
},
|
||||
shadow_md: {
|
||||
...atoms.shadow_md,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(${hues.primary}, 28%, 6%)`,
|
||||
},
|
||||
shadow_lg: {
|
||||
...atoms.shadow_lg,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(${hues.primary}, 28%, 6%)`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
lightPalette,
|
||||
darkPalette,
|
||||
dimPalette,
|
||||
light,
|
||||
dark,
|
||||
dim,
|
||||
}
|
||||
export const themes = {
|
||||
lightPalette: DEFAULT_THEMES.light.palette,
|
||||
darkPalette: DEFAULT_THEMES.dark.palette,
|
||||
dimPalette: DEFAULT_THEMES.dim.palette,
|
||||
light: DEFAULT_THEMES.light,
|
||||
dark: DEFAULT_THEMES.dark,
|
||||
dim: DEFAULT_THEMES.dim,
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const lightPalette = DEFAULT_THEMES.light.palette
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const darkPalette = DEFAULT_THEMES.dark.palette
|
||||
/**
|
||||
* @deprecated use ALF and access palette from `useTheme()`
|
||||
*/
|
||||
export const dimPalette = DEFAULT_THEMES.dim.palette
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const light = DEFAULT_THEMES.light
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const dark = DEFAULT_THEMES.dark
|
||||
/**
|
||||
* @deprecated use ALF and access theme from `useTheme()`
|
||||
*/
|
||||
export const dim = DEFAULT_THEMES.dim
|
||||
|
||||
@@ -1,61 +1,10 @@
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {tokens} from '@bsky.app/alf'
|
||||
|
||||
export const TRACKING = isAndroid ? 0.1 : 0
|
||||
export * from '@bsky.app/alf/dist/tokens'
|
||||
|
||||
export const color = {
|
||||
temp_purple: 'rgb(105 0 255)',
|
||||
temp_purple_dark: 'rgb(83 0 202)',
|
||||
} as const
|
||||
|
||||
export const space = {
|
||||
_2xs: 2,
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
xl: 20,
|
||||
_2xl: 24,
|
||||
_3xl: 28,
|
||||
_4xl: 32,
|
||||
_5xl: 40,
|
||||
} as const
|
||||
|
||||
export const fontSize = {
|
||||
_2xs: 10,
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 18,
|
||||
xl: 20,
|
||||
_2xl: 22,
|
||||
_3xl: 26,
|
||||
_4xl: 32,
|
||||
_5xl: 40,
|
||||
} as const
|
||||
|
||||
export const lineHeight = {
|
||||
none: 1,
|
||||
normal: 1.5,
|
||||
relaxed: 1.625,
|
||||
} as const
|
||||
|
||||
export const borderRadius = {
|
||||
_2xs: 2,
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
full: 999,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* These correspond to Inter font files we actually load.
|
||||
*/
|
||||
export const fontWeight = {
|
||||
normal: '400',
|
||||
medium: '500',
|
||||
bold: '600',
|
||||
heavy: '800',
|
||||
temp_purple: tokens.labelerColor.purple,
|
||||
temp_purple_dark: tokens.labelerColor.purple_dark,
|
||||
} as const
|
||||
|
||||
export const gradients = {
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native'
|
||||
|
||||
export type TextStyleProp = {
|
||||
style?: StyleProp<TextStyle>
|
||||
}
|
||||
|
||||
export type ViewStyleProp = {
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export type ThemeName = 'light' | 'dim' | 'dark'
|
||||
export type Palette = {
|
||||
white: string
|
||||
black: string
|
||||
like: string
|
||||
|
||||
contrast_25: string
|
||||
contrast_50: string
|
||||
contrast_100: string
|
||||
contrast_200: string
|
||||
contrast_300: string
|
||||
contrast_400: string
|
||||
contrast_500: string
|
||||
contrast_600: string
|
||||
contrast_700: string
|
||||
contrast_800: string
|
||||
contrast_900: string
|
||||
contrast_950: string
|
||||
contrast_975: string
|
||||
|
||||
primary_25: string
|
||||
primary_50: string
|
||||
primary_100: string
|
||||
primary_200: string
|
||||
primary_300: string
|
||||
primary_400: string
|
||||
primary_500: string
|
||||
primary_600: string
|
||||
primary_700: string
|
||||
primary_800: string
|
||||
primary_900: string
|
||||
primary_950: string
|
||||
primary_975: string
|
||||
|
||||
positive_25: string
|
||||
positive_50: string
|
||||
positive_100: string
|
||||
positive_200: string
|
||||
positive_300: string
|
||||
positive_400: string
|
||||
positive_500: string
|
||||
positive_600: string
|
||||
positive_700: string
|
||||
positive_800: string
|
||||
positive_900: string
|
||||
positive_950: string
|
||||
positive_975: string
|
||||
|
||||
negative_25: string
|
||||
negative_50: string
|
||||
negative_100: string
|
||||
negative_200: string
|
||||
negative_300: string
|
||||
negative_400: string
|
||||
negative_500: string
|
||||
negative_600: string
|
||||
negative_700: string
|
||||
negative_800: string
|
||||
negative_900: string
|
||||
negative_950: string
|
||||
negative_975: string
|
||||
}
|
||||
export type ThemedAtoms = {
|
||||
text: {
|
||||
color: string
|
||||
}
|
||||
text_contrast_low: {
|
||||
color: string
|
||||
}
|
||||
text_contrast_medium: {
|
||||
color: string
|
||||
}
|
||||
text_contrast_high: {
|
||||
color: string
|
||||
}
|
||||
text_inverted: {
|
||||
color: string
|
||||
}
|
||||
bg: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_25: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_50: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_100: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_200: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_300: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_400: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_500: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_600: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_700: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_800: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_900: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_950: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_975: {
|
||||
backgroundColor: string
|
||||
}
|
||||
border_contrast_low: {
|
||||
borderColor: string
|
||||
}
|
||||
border_contrast_medium: {
|
||||
borderColor: string
|
||||
}
|
||||
border_contrast_high: {
|
||||
borderColor: string
|
||||
}
|
||||
shadow_sm: {
|
||||
shadowRadius: number
|
||||
shadowOpacity: number
|
||||
elevation: number
|
||||
shadowColor: string
|
||||
}
|
||||
shadow_md: {
|
||||
shadowRadius: number
|
||||
shadowOpacity: number
|
||||
elevation: number
|
||||
shadowColor: string
|
||||
}
|
||||
shadow_lg: {
|
||||
shadowRadius: number
|
||||
shadowOpacity: number
|
||||
elevation: number
|
||||
shadowColor: string
|
||||
}
|
||||
}
|
||||
export type Theme = {
|
||||
scheme: 'light' | 'dark' // for library support
|
||||
name: ThemeName
|
||||
palette: Palette
|
||||
atoms: ThemedAtoms
|
||||
}
|
||||
@@ -8,21 +8,6 @@ import {isNative} from '#/platform/detection'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {type Alf, applyFonts, atoms, flatten} from '#/alf'
|
||||
|
||||
/**
|
||||
* Util to calculate lineHeight from a text size atom and a leading atom
|
||||
*
|
||||
* Example:
|
||||
* `leading(atoms.text_md, atoms.leading_normal)` // => 24
|
||||
*/
|
||||
export function leading<
|
||||
Size extends {fontSize?: number},
|
||||
Leading extends {lineHeight?: number},
|
||||
>(textSize: Size, leading: Leading) {
|
||||
const size = textSize?.fontSize || atoms.text_md.fontSize
|
||||
const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight
|
||||
return Math.round(size * lineHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that `lineHeight` defaults to a relative value of `1`, or applies
|
||||
* other relative leading atoms.
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import {jest} from '@jest/globals'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {transparentifyColor} from '../colorGeneration'
|
||||
|
||||
jest.mock('#/logger', () => ({
|
||||
logger: {warn: jest.fn()},
|
||||
}))
|
||||
|
||||
describe('transparentifyColor', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
@@ -41,8 +36,5 @@ describe('transparentifyColor', () => {
|
||||
const unsupported = 'blue'
|
||||
const result = transparentifyColor(unsupported, 0.5)
|
||||
expect(result).toBe(unsupported)
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Could not make '${unsupported}' transparent`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,49 +1,8 @@
|
||||
import {logger} from '#/logger'
|
||||
import {utils} from '@bsky.app/alf'
|
||||
|
||||
export const BLUE_HUE = 211
|
||||
export const RED_HUE = 346
|
||||
export const GREEN_HUE = 152
|
||||
|
||||
/**
|
||||
* Smooth progression of lightness "stops" for generating HSL colors.
|
||||
* @deprecated use `utils.alpha` from `@bsky.app/alf` instead
|
||||
*/
|
||||
export const COLOR_STOPS = [
|
||||
0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1,
|
||||
]
|
||||
|
||||
export function generateScale(start: number, end: number) {
|
||||
const range = end - start
|
||||
return COLOR_STOPS.map(stop => {
|
||||
return start + range * stop
|
||||
})
|
||||
}
|
||||
|
||||
export const defaultScale = generateScale(6, 100)
|
||||
// dim shifted 6% lighter
|
||||
export const dimScale = generateScale(12, 100)
|
||||
|
||||
export function transparentifyColor(color: string, alpha: number) {
|
||||
if (color.startsWith('hsl(')) {
|
||||
return 'hsla(' + color.slice('hsl('.length, -1) + `, ${alpha})`
|
||||
} else if (color.startsWith('rgb(')) {
|
||||
return 'rgba(' + color.slice('rgb('.length, -1) + `, ${alpha})`
|
||||
} else if (color.startsWith('#')) {
|
||||
if (color.length === 7) {
|
||||
const alphaHex = Math.round(alpha * 255).toString(16)
|
||||
// Per MDN: If there is only one number, it is duplicated: e means ee
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color
|
||||
return color.slice(0, 7) + alphaHex.padStart(2, alphaHex)
|
||||
} else if (color.length === 4) {
|
||||
// convert to 6-digit hex before adding alpha
|
||||
const [r, g, b] = color.slice(1).split('')
|
||||
const alphaHex = Math.round(alpha * 255).toString(16)
|
||||
return `#${r.repeat(2)}${g.repeat(2)}${b.repeat(2)}${alphaHex.padStart(
|
||||
2,
|
||||
alphaHex,
|
||||
)}`
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Could not make '${color}' transparent`)
|
||||
}
|
||||
return color
|
||||
}
|
||||
export const transparentifyColor = utils.alpha
|
||||
|
||||
@@ -1,62 +1 @@
|
||||
import {Platform} from 'react-native'
|
||||
|
||||
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
|
||||
|
||||
/**
|
||||
* Identity function on web. Returns nothing on other platforms.
|
||||
*
|
||||
* Note: Platform splitting does not tree-shake away the other platforms,
|
||||
* so don't do stuff like e.g. rely on platform-specific imports. Use
|
||||
* platform-split files instead.
|
||||
*/
|
||||
export function web(value: any) {
|
||||
if (isWeb) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity function on iOS. Returns nothing on other platforms.
|
||||
*
|
||||
* Note: Platform splitting does not tree-shake away the other platforms,
|
||||
* so don't do stuff like e.g. rely on platform-specific imports. Use
|
||||
* platform-split files instead.
|
||||
*/
|
||||
export function ios(value: any) {
|
||||
if (isIOS) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity function on Android. Returns nothing on other platforms..
|
||||
*
|
||||
* Note: Platform splitting does not tree-shake away the other platforms,
|
||||
* so don't do stuff like e.g. rely on platform-specific imports. Use
|
||||
* platform-split files instead.
|
||||
*/
|
||||
export function android(value: any) {
|
||||
if (isAndroid) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity function on iOS and Android. Returns nothing on web.
|
||||
*
|
||||
* Note: Platform splitting does not tree-shake away the other platforms,
|
||||
* so don't do stuff like e.g. rely on platform-specific imports. Use
|
||||
* platform-split files instead.
|
||||
*/
|
||||
export function native(value: any) {
|
||||
if (isNative) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Platform splitting does not tree-shake away the other platforms,
|
||||
* so don't do stuff like e.g. rely on platform-specific imports. Use
|
||||
* platform-split files instead.
|
||||
*/
|
||||
export const platform = Platform.select
|
||||
export {android, ios, native, platform, web} from '@bsky.app/alf'
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
import {type Theme} from '@bsky.app/alf'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {type Theme} from '../types'
|
||||
|
||||
export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
|
||||
if (isAndroid) {
|
||||
if (themeType === 'theme') {
|
||||
SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
|
||||
} else {
|
||||
SystemUI.setBackgroundColorAsync('black')
|
||||
try {
|
||||
if (themeType === 'theme') {
|
||||
SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
|
||||
} else {
|
||||
SystemUI.setBackgroundColorAsync('black')
|
||||
}
|
||||
} catch (error) {
|
||||
// Can reject with 'The current activity is no longer available' - no big deal
|
||||
logger.debug('Could not set system UI theme', {safeMessage: error})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
import {type ThemeName} from '#/alf/types'
|
||||
import {utils} from '@bsky.app/alf'
|
||||
|
||||
export function select<T>(name: ThemeName, options: Record<ThemeName, T>) {
|
||||
switch (name) {
|
||||
case 'light':
|
||||
return options.light
|
||||
case 'dark':
|
||||
return options.dark || options.dim
|
||||
case 'dim':
|
||||
return options.dim || options.dark
|
||||
default:
|
||||
throw new Error(`select(theme, options) received unknown theme ${name}`)
|
||||
}
|
||||
}
|
||||
export const select = utils.select
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import {type ColorSchemeName, useColorScheme} from 'react-native'
|
||||
import {type ThemeName} from '@bsky.app/alf'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useThemePrefs} from '#/state/shell'
|
||||
import {dark, dim, light} from '#/alf/themes'
|
||||
import {type ThemeName} from '#/alf/types'
|
||||
|
||||
export function useColorModeTheme(): ThemeName {
|
||||
const theme = useThemeName()
|
||||
|
||||
@@ -77,7 +77,7 @@ export function AccountList({
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.font_semi_bold,
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.py_sm,
|
||||
@@ -151,7 +151,7 @@ function AccountItem({
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.font_bold, a.leading_tight]}
|
||||
style={[a.font_semi_bold, a.leading_tight]}
|
||||
numberOfLines={1}>
|
||||
{sanitizeDisplayName(
|
||||
profile?.displayName || profile?.handle || account.handle,
|
||||
@@ -172,7 +172,7 @@ function AccountItem({
|
||||
</View>
|
||||
|
||||
{isCurrentAccount ? (
|
||||
<Check size="sm" style={[{color: t.palette.positive_600}]} />
|
||||
<Check size="sm" style={[{color: t.palette.positive_500}]} />
|
||||
) : (
|
||||
<Chevron size="sm" style={[t.atoms.text]} />
|
||||
)}
|
||||
|
||||
@@ -3,21 +3,18 @@ import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button as BaseButton, type ButtonProps} from '#/components/Button'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
||||
import {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye'
|
||||
import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
|
||||
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {Text as BaseText, type TextProps} from '#/components/Typography'
|
||||
import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSadIcon} from './icons/Emoji'
|
||||
|
||||
export const colors = {
|
||||
warning: {
|
||||
light: '#DFBC00',
|
||||
dark: '#BFAF1F',
|
||||
},
|
||||
warning: '#FFC404',
|
||||
}
|
||||
|
||||
type Context = {
|
||||
type: 'info' | 'tip' | 'warning' | 'error'
|
||||
type: 'info' | 'tip' | 'warning' | 'error' | 'apology'
|
||||
}
|
||||
|
||||
const Context = createContext<Context>({
|
||||
@@ -29,29 +26,46 @@ export function Icon() {
|
||||
const t = useTheme()
|
||||
const {type} = useContext(Context)
|
||||
const Icon = {
|
||||
info: InfoIcon,
|
||||
tip: TipIcon,
|
||||
info: CircleInfoIcon,
|
||||
tip: CircleInfoIcon,
|
||||
warning: WarningIcon,
|
||||
error: ErrorIcon,
|
||||
error: CircleXIcon,
|
||||
apology: EmojiSadIcon,
|
||||
}[type]
|
||||
const fill = {
|
||||
info: t.atoms.text_contrast_medium.color,
|
||||
tip: t.palette.primary_500,
|
||||
warning: colors.warning.light,
|
||||
warning: colors.warning,
|
||||
error: t.palette.negative_500,
|
||||
apology: t.atoms.text_contrast_medium.color,
|
||||
}[type]
|
||||
return <Icon fill={fill} size="md" />
|
||||
}
|
||||
|
||||
export function Content({
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
return (
|
||||
<View
|
||||
style={[a.gap_sm, a.flex_1, {minHeight: 20}, a.justify_center, style]}
|
||||
{...rest}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Text({
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
}: Pick<TextProps, 'children' | 'style'>) {
|
||||
return (
|
||||
<BaseText
|
||||
{...rest}
|
||||
style={[a.flex_1, a.text_sm, a.leading_snug, a.pr_md, style]}>
|
||||
<BaseText {...rest} style={[a.text_sm, a.leading_snug, a.pr_md, style]}>
|
||||
{children}
|
||||
</BaseText>
|
||||
)
|
||||
@@ -60,17 +74,23 @@ export function Text({
|
||||
export function Button({
|
||||
children,
|
||||
...props
|
||||
}: Omit<ButtonProps, 'size' | 'variant' | 'color'>) {
|
||||
}: Omit<ButtonProps, 'size' | 'variant'>) {
|
||||
return (
|
||||
<BaseButton size="tiny" variant="outline" color="secondary" {...props}>
|
||||
<BaseButton size="tiny" {...props}>
|
||||
{children}
|
||||
</BaseButton>
|
||||
)
|
||||
}
|
||||
|
||||
export function Row({children}: {children: React.ReactNode}) {
|
||||
export function Row({
|
||||
children,
|
||||
style,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
return (
|
||||
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<View style={[a.w_full, a.flex_row, a.align_start, a.gap_sm, style]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
@@ -88,19 +108,21 @@ export function Outer({
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const borderColor = {
|
||||
info: t.atoms.border_contrast_low.borderColor,
|
||||
tip: t.atoms.border_contrast_low.borderColor,
|
||||
warning: t.atoms.border_contrast_low.borderColor,
|
||||
error: t.atoms.border_contrast_low.borderColor,
|
||||
info: t.atoms.border_contrast_high.borderColor,
|
||||
tip: t.palette.primary_500,
|
||||
warning: colors.warning,
|
||||
error: t.palette.negative_500,
|
||||
apology: t.atoms.border_contrast_high.borderColor,
|
||||
}[type]
|
||||
return (
|
||||
<Context.Provider value={{type}}>
|
||||
<View
|
||||
style={[
|
||||
gtMobile ? a.p_md : a.p_sm,
|
||||
a.p_md,
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg,
|
||||
{borderColor},
|
||||
style,
|
||||
]}>
|
||||
@@ -123,7 +145,9 @@ export function Admonition({
|
||||
<Outer type={type} style={style}>
|
||||
<Row>
|
||||
<Icon />
|
||||
<Text>{children}</Text>
|
||||
<Content>
|
||||
<Text>{children}</Text>
|
||||
</Content>
|
||||
</Row>
|
||||
</Outer>
|
||||
)
|
||||
|
||||
@@ -96,7 +96,7 @@ export function BlockedGeoOverlay() {
|
||||
<Mark fill={t.palette.primary_600} width={14} />
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.font_semi_bold,
|
||||
{
|
||||
color: t.palette.primary_600,
|
||||
},
|
||||
@@ -121,8 +121,7 @@ export function BlockedGeoOverlay() {
|
||||
</View>
|
||||
|
||||
<View style={[a.mt_xl, a.align_start]}>
|
||||
<Text
|
||||
style={[a.text_lg, a.font_heavy, a.leading_snug, a.pb_xs]}>
|
||||
<Text style={[a.text_lg, a.font_bold, a.leading_snug, a.pb_xs]}>
|
||||
<Trans>Not in Mississippi?</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Pressable,
|
||||
type PressableProps,
|
||||
type StyleProp,
|
||||
StyleSheet,
|
||||
type TargetedEvent,
|
||||
type TextProps,
|
||||
type TextStyle,
|
||||
@@ -240,7 +239,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
}
|
||||
} else if (color === 'secondary') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg_contrast_25)
|
||||
baseStyles.push(t.atoms.bg_contrast_50)
|
||||
hoverStyles.push(t.atoms.bg_contrast_100)
|
||||
} else {
|
||||
baseStyles.push(t.atoms.bg_contrast_50)
|
||||
@@ -274,51 +273,27 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
} else if (color === 'primary_subtle') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_50,
|
||||
dim: t.palette.primary_100,
|
||||
dark: t.palette.primary_100,
|
||||
}),
|
||||
backgroundColor: t.palette.primary_50,
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_100,
|
||||
dim: t.palette.primary_200,
|
||||
dark: t.palette.primary_200,
|
||||
}),
|
||||
backgroundColor: t.palette.primary_100,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_25,
|
||||
dim: t.palette.primary_50,
|
||||
dark: t.palette.primary_50,
|
||||
}),
|
||||
backgroundColor: t.palette.primary_50,
|
||||
})
|
||||
}
|
||||
} else if (color === 'negative_subtle') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.negative_50,
|
||||
dim: t.palette.negative_100,
|
||||
dark: t.palette.negative_100,
|
||||
}),
|
||||
backgroundColor: t.palette.negative_50,
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.negative_100,
|
||||
dim: t.palette.negative_200,
|
||||
dark: t.palette.negative_200,
|
||||
}),
|
||||
backgroundColor: t.palette.negative_100,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.negative_25,
|
||||
dim: t.palette.negative_50,
|
||||
dark: t.palette.negative_50,
|
||||
}),
|
||||
backgroundColor: t.palette.negative_50,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -372,7 +347,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -396,7 +371,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -536,8 +511,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
[state, variant, color, size, disabled],
|
||||
)
|
||||
|
||||
const flattenedBaseStyles = flatten([baseStyles, style])
|
||||
|
||||
return (
|
||||
<PressableComponent
|
||||
role="button"
|
||||
@@ -557,9 +530,10 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.curve_continuous,
|
||||
flattenedBaseStyles,
|
||||
baseStyles,
|
||||
style,
|
||||
...(state.hovered || state.pressed
|
||||
? [hoverStyles, flatten(hoverStyleProp)]
|
||||
? [hoverStyles, hoverStyleProp]
|
||||
: []),
|
||||
]}
|
||||
onPressIn={onPressIn}
|
||||
@@ -626,37 +600,21 @@ export function useSharedButtonTextStyles() {
|
||||
} else if (color === 'primary_subtle') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.primary_600,
|
||||
dim: t.palette.primary_800,
|
||||
dark: t.palette.primary_800,
|
||||
}),
|
||||
color: t.palette.primary_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.primary_200,
|
||||
dim: t.palette.primary_200,
|
||||
dark: t.palette.primary_200,
|
||||
}),
|
||||
color: t.palette.primary_200,
|
||||
})
|
||||
}
|
||||
} else if (color === 'negative_subtle') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.negative_600,
|
||||
dim: t.palette.negative_800,
|
||||
dark: t.palette.negative_800,
|
||||
}),
|
||||
color: t.palette.negative_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: select(t.name, {
|
||||
light: t.palette.negative_200,
|
||||
dim: t.palette.negative_200,
|
||||
dark: t.palette.negative_200,
|
||||
}),
|
||||
color: t.palette.negative_200,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -763,10 +721,10 @@ export function useSharedButtonTextStyles() {
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push(a.text_sm, a.leading_snug, a.font_medium)
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push(a.text_xs, a.leading_snug, a.font_medium)
|
||||
baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold)
|
||||
}
|
||||
|
||||
return StyleSheet.flatten(baseStyles)
|
||||
return flatten(baseStyles)
|
||||
}, [t, variant, color, size, disabled])
|
||||
}
|
||||
|
||||
@@ -877,3 +835,47 @@ export function ButtonIcon({
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export type StackedButtonProps = Omit<
|
||||
ButtonProps,
|
||||
keyof VariantProps | 'children'
|
||||
> &
|
||||
Pick<VariantProps, 'color'> & {
|
||||
children: React.ReactNode
|
||||
icon: React.ComponentType<SVGIconProps>
|
||||
}
|
||||
|
||||
export function StackedButton({children, ...props}: StackedButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
size="tiny"
|
||||
style={[
|
||||
a.flex_col,
|
||||
{
|
||||
height: 72,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 20,
|
||||
gap: 4,
|
||||
},
|
||||
props.style,
|
||||
]}>
|
||||
<StackedButtonInnerText icon={props.icon}>
|
||||
{children}
|
||||
</StackedButtonInnerText>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function StackedButtonInnerText({
|
||||
children,
|
||||
icon: Icon,
|
||||
}: Pick<StackedButtonProps, 'icon' | 'children'>) {
|
||||
const textStyles = useSharedButtonTextStyles()
|
||||
return (
|
||||
<>
|
||||
<Icon width={24} fill={textStyles.color} />
|
||||
<ButtonText>{children}</ButtonText>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -796,7 +796,7 @@ export function ItemText({children, style}: ItemTextProps) {
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text_contrast_high,
|
||||
{paddingTop: 3},
|
||||
style,
|
||||
@@ -855,7 +855,11 @@ export function LabelText({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text
|
||||
style={[a.font_bold, t.atoms.text_contrast_medium, {marginBottom: -8}]}>
|
||||
style={[
|
||||
a.font_semi_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
{marginBottom: -8},
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import {TouchableWithoutFeedback, View} from 'react-native'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useDevMode} from '#/storage/hooks/dev-mode'
|
||||
|
||||
/**
|
||||
* Internal-use component to display debug information supplied by the appview.
|
||||
* The `debug` field only exists on some API views, and is only visible for
|
||||
* internal users in dev mode. As such, none of these strings need to be
|
||||
* translated.
|
||||
*
|
||||
* This component can be removed at any time if we don't find it useful.
|
||||
*/
|
||||
export function DebugFieldDisplay<T extends {debug?: {[x: string]: unknown}}>({
|
||||
subject,
|
||||
}: {
|
||||
subject: T
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const [devMode] = useDevMode()
|
||||
const prompt = Prompt.usePromptControl()
|
||||
|
||||
if (!devMode) return
|
||||
if (!subject.debug) return
|
||||
|
||||
return (
|
||||
<>
|
||||
<Prompt.Basic
|
||||
control={prompt}
|
||||
title="Debug"
|
||||
description={JSON.stringify(subject.debug, null, 2)}
|
||||
cancelButtonCta="Close"
|
||||
confirmButtonCta="Copy"
|
||||
onConfirm={() => {
|
||||
Clipboard.setStringAsync(JSON.stringify(subject.debug, null, 2))
|
||||
Toast.show('Copied to clipboard', {type: 'success'})
|
||||
}}
|
||||
/>
|
||||
<TouchableWithoutFeedback
|
||||
accessibilityRole="button"
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
prompt.open()
|
||||
return false
|
||||
}}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs, a.pt_sm, a.pb_xs]}>
|
||||
<View
|
||||
style={[a.py_xs, a.px_sm, a.rounded_sm, t.atoms.bg_contrast_25]}>
|
||||
<Text
|
||||
style={[a.font_bold, a.text_xs, t.atoms.text_contrast_medium]}>
|
||||
Debug
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_xs,
|
||||
a.leading_tight,
|
||||
{fontFamily: 'monospace'},
|
||||
t.atoms.text_contrast_low,
|
||||
]}>
|
||||
{JSON.stringify(subject.debug)}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
||||
import {logger} from '#/logger'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {Context} from '#/components/Dialog/context'
|
||||
import {
|
||||
@@ -180,7 +180,7 @@ export function Inner({
|
||||
onClick={stopPropagation}
|
||||
onStartShouldSetResponder={_ => true}
|
||||
onTouchEnd={stopPropagation}
|
||||
style={flatten([
|
||||
style={[
|
||||
a.relative,
|
||||
a.rounded_md,
|
||||
a.w_full,
|
||||
@@ -195,7 +195,7 @@ export function Inner({
|
||||
},
|
||||
!reduceMotionEnabled && a.zoom_fade_in,
|
||||
style,
|
||||
])}>
|
||||
]}>
|
||||
<DismissableLayer.DismissableLayer
|
||||
onInteractOutside={preventDefault}
|
||||
onFocusOutside={preventDefault}
|
||||
@@ -244,7 +244,7 @@ export const InnerFlatList = React.forwardRef<
|
||||
contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}>
|
||||
<FlatList
|
||||
ref={ref}
|
||||
style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, flatten(style)]}
|
||||
style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, style]}
|
||||
{...props}
|
||||
/>
|
||||
{footer}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function HeaderText({
|
||||
style?: StyleProp<TextStyle>
|
||||
}) {
|
||||
return (
|
||||
<Text style={[a.text_lg, a.text_center, a.font_heavy, style]}>
|
||||
<Text style={[a.text_lg, a.text_center, a.font_semi_bold, style]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ export function Error({
|
||||
]}
|
||||
sideBorders={sideBorders}>
|
||||
<View style={[a.w_full, a.align_center, a.gap_lg]}>
|
||||
<Text style={[a.font_bold, a.text_3xl]}>{title}</Text>
|
||||
<Text style={[a.font_semi_bold, a.text_3xl]}>{title}</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
|
||||
@@ -128,7 +128,7 @@ export function TitleAndByline({
|
||||
<View style={[a.flex_1]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_md, a.font_bold, a.leading_snug]}
|
||||
style={[a.text_md, a.font_semi_bold, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
@@ -186,7 +186,7 @@ export function Description({
|
||||
return rt
|
||||
}, [description])
|
||||
if (!rt) return null
|
||||
return <RichText value={rt} style={[a.leading_snug]} disableLinks {...rest} />
|
||||
return <RichText value={rt} disableLinks {...rest} />
|
||||
}
|
||||
|
||||
export function DescriptionPlaceholder() {
|
||||
@@ -214,7 +214,7 @@ export function DescriptionPlaceholder() {
|
||||
export function Likes({count}: {count: number}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.font_bold]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.font_semi_bold]}>
|
||||
<Trans>
|
||||
Liked by <Plural value={count || 0} one="# user" other="# users" />
|
||||
</Trans>
|
||||
|
||||
@@ -380,7 +380,7 @@ export function ProfileGrid({
|
||||
a.justify_between,
|
||||
]}
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
<Text style={[a.text_sm, a.font_bold, t.atoms.text]}>
|
||||
<Text style={[a.text_sm, a.font_semi_bold, t.atoms.text]}>
|
||||
{isFeedContext ? (
|
||||
<Trans>Suggested for you</Trans>
|
||||
) : (
|
||||
@@ -390,7 +390,12 @@ export function ProfileGrid({
|
||||
{!isProfileHeaderContext && (
|
||||
<InlineLinkText
|
||||
label={_(msg`See more suggested profiles on the Explore page`)}
|
||||
to="/search">
|
||||
to="/search"
|
||||
onPress={() => {
|
||||
logger.metric('suggestedUser:seeMore', {
|
||||
logContext: isFeedContext ? 'Explore' : 'Profile',
|
||||
})
|
||||
}}>
|
||||
<Trans>See more</Trans>
|
||||
</InlineLinkText>
|
||||
)}
|
||||
@@ -429,6 +434,11 @@ function SeeMoreSuggestedProfilesCard() {
|
||||
to="/search"
|
||||
color="primary"
|
||||
label={_(msg`Browse more accounts on the Explore page`)}
|
||||
onPress={() => {
|
||||
logger.metric('suggestedUser:seeMore', {
|
||||
logContext: 'Explore',
|
||||
})
|
||||
}}
|
||||
style={[
|
||||
a.flex_col,
|
||||
a.align_center,
|
||||
@@ -516,7 +526,7 @@ export function SuggestedFeeds() {
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_lg,
|
||||
a.font_bold,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>Some other feeds you might like</Trans>
|
||||
|
||||