Compare commits

..

34 Commits

Author SHA1 Message Date
Eric Bailey b963ce8b8a Simple feed-end component with trends on mobile
(cherry picked from commit ccfea4b9461111701586f39e8e3e1bfad187708c)
2024-12-17 17:12:44 -06:00
Eric Bailey 30207577b7 Remove stray flex_1 2024-12-17 17:11:48 -06:00
Eric Bailey 511a13ec2f Remove comment 2024-12-17 17:11:48 -06:00
Eric Bailey d0b6000d22 Remove old file 2024-12-17 17:11:48 -06:00
Eric Bailey b3f253dbba Up config query to 5 min 2024-12-17 17:11:48 -06:00
Eric Bailey 90ed779cc3 Fix no-show logic 2024-12-17 17:11:48 -06:00
Eric Bailey eda3c35155 Hook up prod agent 2024-12-17 17:11:48 -06:00
Eric Bailey 7cb9694971 Straight passthrough links 2024-12-17 17:11:48 -06:00
Eric Bailey 87e88c31e8 Tweak config 2024-12-17 17:11:48 -06:00
Eric Bailey 3bf151d9b6 Hook up real data 2024-12-17 17:11:47 -06:00
Eric Bailey b269b1979d Only english during beta period 2024-12-17 17:11:47 -06:00
Eric Bailey 4a8fb8255d Add feature gate 2024-12-17 17:11:47 -06:00
Eric Bailey cd52e1bcda Update package, rm langs 2024-12-17 17:11:47 -06:00
Eric Bailey 57ff6f184d Gate settings too 2024-12-17 17:11:47 -06:00
Eric Bailey 305a9e6346 Remove titleChild 2024-12-17 17:11:47 -06:00
Eric Bailey 369373d2aa Remove broadcast hack 2024-12-17 17:11:47 -06:00
Eric Bailey 82a069fdaf Cache enabled value for reloads, use real data for service config 2024-12-17 17:11:47 -06:00
Eric Bailey e03a52b6c4 Stale time to 30m 2024-12-17 17:11:47 -06:00
Eric Bailey 2f57c24e5b Centralize mute word handling 2024-12-17 17:11:47 -06:00
Eric Bailey 2097b67ff0 Handle service config, enablement, load states, update lex contract 2024-12-17 17:11:47 -06:00
Eric Bailey 9d7ebe3ee7 Some skeleton UI 2024-12-17 17:11:47 -06:00
Eric Bailey 111ca28fb9 Align components 2024-12-17 17:11:47 -06:00
Eric Bailey c8a6e3b323 Wire up interstitial and Explore page 2024-12-17 17:11:47 -06:00
Eric Bailey e92095810d First hack at mute words 2024-12-17 17:11:47 -06:00
Eric Bailey 72cf85d38b Exploration 2024-12-17 17:11:47 -06:00
Eric Bailey b03a85ad53 Add new interstitial to Discover 2024-12-17 17:11:47 -06:00
Eric Bailey be3c36fcf6 Clean up persisted code, add new setting 2024-12-17 17:11:47 -06:00
Eric Bailey 99db939beb Fix up hide/show toggle state, WITH broadcast hacK 2024-12-17 17:11:47 -06:00
Eric Bailey e3d110207e Add to settings, abstract state, not updating in tab 2024-12-17 17:11:47 -06:00
Eric Bailey 55b573a35a Add persisted option to hide trending from sidebar 2024-12-17 17:11:47 -06:00
Eric Bailey 0ff6151ab8 Add tiny option in RightNav 2024-12-17 17:11:47 -06:00
Eric Bailey 6287bf6c44 Improve explore treatment a bit, add some polish to cards 2024-12-17 17:11:47 -06:00
Eric Bailey 352302a1a4 Disable SPs for now 2024-12-17 17:11:47 -06:00
Paul Frazee 7dd6f6c3dc Add WIP UIs for trending topics and suggested starterpacks 2024-12-17 17:11:47 -06:00
524 changed files with 99225 additions and 210976 deletions
+14 -2
View File
@@ -32,7 +32,6 @@ module.exports = {
'H6',
'P',
'Admonition',
'Admonition.Admonition',
],
impliedTextProps: [],
suggestedTextWrappers: {
@@ -81,6 +80,19 @@ module.exports = {
],
'simple-import-sort/exports': 'error',
'react-compiler/react-compiler': 'warn',
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@atproto/api',
importNames: ['moderatePost'],
message:
'Please use `moderatePost_wrapped` from `#/lib/moderatePost_wrapped` instead.',
},
],
},
],
},
ignorePatterns: [
'**/__mocks__/*.ts',
@@ -92,9 +104,9 @@ module.exports = {
'*.lock',
'.husky',
'patches',
'bskyweb',
'*.html',
'bskyweb',
'bskyembed',
'src/locale/locales/_build/',
'src/locale/locales/**/*.js',
],
@@ -64,7 +64,6 @@ jobs:
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "$json" > google-services.json
- name: 🏗️ EAS Build
+2 -3
View File
@@ -14,7 +14,7 @@ on:
jobs:
build:
name: Build and Submit iOS
runs-on: macos-15
runs-on: macos-14
steps:
- name: Check for EXPO_TOKEN
run: >
@@ -52,7 +52,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.2'
xcode-version: '15.3'
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
@@ -76,7 +76,6 @@ jobs:
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
@@ -69,9 +69,6 @@ jobs:
- name: Lint check
run: yarn lint
- name: Lint lockfile
run: yarn lockfile-lint
- name: Prettier check
run: yarn prettier --check .
@@ -104,7 +101,6 @@ jobs:
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "$json" > google-services.json
- name: 🏗️ Create Bundle
@@ -134,7 +130,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-14
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
cancel-in-progress: true
@@ -176,7 +172,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.2'
xcode-version: '15.3'
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
@@ -200,7 +196,6 @@ jobs:
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
@@ -227,7 +222,7 @@ jobs:
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-android
cancel-in-progress: false
needs: [bundleDeploy]
needs: [ bundleDeploy ]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
@@ -278,7 +273,6 @@ jobs:
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
echo "$json" > google-services.json
- name: 🏗️ EAS Build
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
- name: Set up Go tooling
uses: actions/setup-go@v3
with:
go-version: '1.23'
go-version: '1.22'
- name: Dummy Static Files
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
- name: Check
@@ -36,7 +36,7 @@ jobs:
- name: Set up Go tooling
uses: actions/setup-go@v3
with:
go-version: '1.23'
go-version: '1.22'
- name: Dummy Static Files
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
- name: Lint
-2
View File
@@ -28,8 +28,6 @@ jobs:
attempt_delay: 2000
- name: Lint check
run: yarn lint
- name: Lint lockfile
run: yarn lockfile-lint
- name: Prettier check
run: yarn prettier --check .
- name: Check & compile i18n
@@ -1,49 +0,0 @@
name: Nightly Update Source Languages
on:
schedule:
- cron: '0 2 * * *' # run at 2 AM UTC
workflow_dispatch:
jobs:
extract-languages:
name: Nightly Update Source Languages
runs-on: ubuntu-latest
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
steps:
- name: Check out Git repository
uses: actions/checkout@v3
with:
ssh-key: ${{secrets.GH_ACTION_DEPLOY_KEY}}
- name: Install node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Yarn install
uses: Wandalen/wretry.action@master
with:
command: yarn --frozen-lockfile
attempt_limit: 3
attempt_delay: 2000
- name: Extract language strings
run: yarn intl:extract
- name: Create commit
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Nightly source-language update
file_pattern: ./src/locale/locales/en/messages.po
- name: Push source lang to Crowdin
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_sources_args: "-b main"
upload_translations: false
push_translations: false
push_sources: false
create_pull_request: false
env:
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
-46
View File
@@ -1,46 +0,0 @@
name: Lockfile
on:
pull_request:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
jobs:
verify-yarn-lock:
name: No manual yarn.lock edits
runs-on: ubuntu-latest
steps:
- name: Check out PR HEAD
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Fetch base branch
run: git fetch origin ${{ github.base_ref }} --depth=1
- name: Install node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Reset yarn.lock to base
run: git show "origin/${{ github.base_ref }}:yarn.lock" > yarn.lock
- name: Yarn install
uses: Wandalen/wretry.action@master
with:
# Fine to skip scripts since we don't run any code
command: yarn install --ignore-scripts
attempt_limit: 3
attempt_delay: 2000
- name: Verify yarn.lock
run: |
git diff --quiet --exit-code || {
echo '::error::`yarn.lock` does not match what Yarn would generate given the base `yarn.lock` and the head `package.json`.'
echo '::error:: - If this is intentional, you can ignore this check.'
echo '::error:: - If this is unintentional, apply the following diff:'
git --no-pager diff
exit 1
}
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.23-bullseye AS build-env
FROM golang:1.22-bullseye AS build-env
WORKDIR /usr/src/social-app
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.23-bullseye AS build-env
FROM golang:1.22-bullseye AS build-env
WORKDIR /usr/src/social-app
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 20232025 Bluesky PBC
Copyright 20232024 Bluesky PBC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+2 -2
View File
@@ -37,6 +37,6 @@ deps: ## Installs dependent libs using 'yarn install'
.PHONY: nvm-setup
nvm-setup: ## Use NVM to install and activate node+yarn
nvm install 20
nvm use 20
nvm install 18
nvm use 18
npm install --global yarn
+1 -1
View File
@@ -1,2 +1,2 @@
flows:
- "flows/**"
- "flows/*"
+1 -1
View File
@@ -17,7 +17,7 @@ appId: xyz.blueskyweb.app
- inputText: "Post with an image"
- tapOn:
id: "openGalleryBtn"
- tapOn: "Allow Full Access"
- tapOn: "Content warnings"
- tapOn: "Content warnings"
- tapOn: "Porn"
- tapOn:
+6 -3
View File
@@ -10,7 +10,8 @@ appId: xyz.blueskyweb.app
id: "e2eOpenLoggedOutView"
- tapOn:
id: "createAccountButton"
- tapOn: "Bluesky Social"
- tapOn:
id: "selectServiceButton"
- tapOn:
id: "customSelectBtn"
- tapOn:
@@ -24,10 +25,12 @@ appId: xyz.blueskyweb.app
- inputText: "example@test.com"
- tapOn:
id: "passwordInput"
- inputText: "hunter22"
- hideKeyboard
- inputText: "hunter2"
- pressKey: Enter
- tapOn:
id: "nextBtn"
- tapOn:
id: "handleInput"
- inputText: "e2e-test"
- tapOn:
id: "nextBtn"
+9 -6
View File
@@ -26,7 +26,7 @@ appId: xyz.blueskyweb.app
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- tapOn: "People"
- tapOn: "About"
- assertVisible: "Good Ppl"
- assertVisible: "They good"
@@ -74,6 +74,7 @@ appId: xyz.blueskyweb.app
- tapOn: "Delete List"
- tapOn:
id: "confirmBtn"
- assertVisible: "This list is empty!"
- tapOn:
label: "Create a new curatelist"
@@ -92,12 +93,14 @@ appId: xyz.blueskyweb.app
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- tapOn: "People"
- tapOn: "About"
- assertVisible: "Good Ppl"
- assertVisible: "They good"
- tapOn: "People"
- tapOn: "About"
- tapOn: "Start adding people"
- tapOn:
label: "Adds users on curatelists from the list"
id: "addUserBtn"
- assertVisible:
id: "listAddUserModal"
- tapOn:
@@ -142,7 +145,7 @@ appId: xyz.blueskyweb.app
id: "e2eGotoLists"
- tapOn: "Good Ppl"
- tapOn: "People"
- tapOn: "About"
- assertVisible:
label: "Removes users on curatelists from the list"
id: "user-bob.test"
@@ -160,7 +163,7 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Adds and removes users on curatelists from the profile"
id: "bottomBarSearchBtn"
- tapOn: "Search for posts, users, or feeds"
- tapOn: "Search"
- inputText: "bob"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
+17 -9
View File
@@ -11,12 +11,14 @@ appId: xyz.blueskyweb.app
# Pin alice's feed
- extendedWaitUntil:
visible: "Open drawer menu"
- tapOn: "Open drawer menu"
visible:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "profileCardButton"
- tapOn:
id: "profilePager-selector-5"
id: "profilePager-selector-4"
- tapOn: "alice-favs"
- tapOn: "Pin to Home"
- tapOn:
@@ -30,7 +32,8 @@ appId: xyz.blueskyweb.app
text: "alice-favs"
# Set alice-favs first
- tapOn: "Open drawer menu"
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
@@ -41,7 +44,8 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
@@ -50,7 +54,8 @@ appId: xyz.blueskyweb.app
text: "Following"
# Set following first
- tapOn: "Open drawer menu"
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
@@ -61,7 +66,8 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "Following"
@@ -70,7 +76,8 @@ appId: xyz.blueskyweb.app
text: "alice-favs"
# Remove following
- tapOn: "Open drawer menu"
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
@@ -81,7 +88,8 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn: "Go back"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
+2 -4
View File
@@ -21,15 +21,13 @@ appId: xyz.blueskyweb.app
label: "Feeds button disappears after pinning a feed"
id: "bottomBarProfileBtn"
- tapOn:
id: "profilePager-selector-5"
id: "profilePager-selector-4"
- tapOn: "alice-favs"
- tapOn: "Pin to Home"
- tapOn:
id: "bottomBarHomeBtn"
- tapOn:
id: "bottomBarHomeBtn"
- tapOn:
id: "bottomBarHomeBtn"
id: "viewHeaderDrawerBtn"
- assertNotVisible: "Feeds ✨"
- tapOn:
+1 -1
View File
@@ -14,7 +14,7 @@ appId: xyz.blueskyweb.app
id: "e2eGotoModeration"
- tapOn:
id: "moderationlistsBtn"
- tapOn: "New list"
- tapOn: "New"
- tapOn:
id: "editNameInput"
- inputText: "Muted Users"
-20
View File
@@ -1,20 +0,0 @@
appId: xyz.blueskyweb.app
---
- runScript:
file: ../setupServer.js
env:
SERVER_PATH: "?users&follows&posts"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- tapOn:
id: "postDropdownBtn"
index: 0
- tapOn:
id: "postDropdownReportBtn"
- tapOn: "Create report for Misleading Post"
- tapOn: "Send report to Dev-env Moderation"
- tapOn:
point: "50%,90%"
+1 -1
View File
@@ -15,7 +15,7 @@ appId: xyz.blueskyweb.app
id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarSearchBtn"
- tapOn: "Search for posts, users, or feeds"
- tapOn: "Search"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
@@ -1,24 +0,0 @@
appId: xyz.blueskyweb.app
---
- runScript:
file: ../../setupServer.js
env:
SERVER_PATH: ?users&follows&posts&feeds
- runFlow:
file: ../../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- tapOn: "Carla's avatar"
- tapOn:
id: "profileHeaderDropdownBtn"
- tapOn:
id: "profileHeaderDropdownReportBtn"
- tapOn:
id: "report:option:com.atproto.moderation.defs#reasonSpam"
- assertVisible:
id: "report:labeler:mod-authority.test"
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
@@ -1,24 +0,0 @@
appId: xyz.blueskyweb.app
---
- runScript:
file: ../../setupServer.js
env:
SERVER_PATH: ?users&follows&posts&feeds
- runFlow:
file: ../../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- tapOn:
id: "postDropdownBtn"
index: 0
- tapOn:
id: "postDropdownReportBtn"
- tapOn:
id: "report:option:com.atproto.moderation.defs#reasonSpam"
- assertVisible:
id: "report:labeler:mod-authority.test"
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
@@ -1,35 +0,0 @@
appId: xyz.blueskyweb.app
---
- runScript:
file: ../../setupServer.js
env:
SERVER_PATH: ?users&follows&posts&feeds
- runFlow:
file: ../../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- tapOn:
id: "postDropdownBtn"
index: 0
- tapOn:
id: "postDropdownReportBtn"
- tapOn:
id: "report:option:com.atproto.moderation.defs#reasonOther"
- assertVisible:
id: "report:labeler:mod-authority.test"
# reason "other" defaults with details open
- assertVisible:
id: "report:details"
- tapOn:
id: "report:clearOption"
- assertNotVisible:
id: "report:details"
- tapOn:
id: "report:option:com.atproto.moderation.defs#reasonSpam"
- assertVisible:
id: "report:labeler:mod-authority.test"
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
@@ -1,30 +0,0 @@
appId: xyz.blueskyweb.app
---
- runScript:
file: ../../setupServer.js
env:
SERVER_PATH: ?users&follows&posts&feeds
- runFlow:
file: ../../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- tapOn:
id: "postDropdownBtn"
index: 0
- tapOn:
id: "postDropdownReportBtn"
- tapOn:
id: "report:option:com.atproto.moderation.defs#reasonOther"
- assertVisible:
id: "report:labeler:mod-authority.test"
# reason "other" defaults with details open
- assertVisible:
id: "report:details"
- tapOn:
id: "report:details"
- inputText: "This is a test report"
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
+2 -2
View File
@@ -12,8 +12,8 @@ appId: xyz.blueskyweb.app
# Navigate to another user profile via autocomplete
- tapOn:
id: "bottomBarSearchBtn"
- assertVisible: "Search for posts, users, or feeds"
- tapOn: "Search for posts, users, or feeds"
- assertVisible: "Search"
- tapOn: "Search"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
-2
View File
@@ -6,8 +6,6 @@ appId: xyz.blueskyweb.app
- waitForAnimationToEnd
- tapOn: "http://localhost:8081"
- waitForAnimationToEnd
- extendedWaitUntil:
visible: "Continue"
- swipe:
from: "Bluesky"
direction: DOWN
-42
View File
@@ -1,42 +0,0 @@
import {IsValidHandle, validateServiceHandle} from '#/lib/strings/handles'
describe('handle validation', () => {
const valid = [
['ali', 'bsky.social'],
['alice', 'bsky.social'],
['a-lice', 'bsky.social'],
['a-----lice', 'bsky.social'],
['123', 'bsky.social'],
['123456789012345678', 'bsky.social'],
['alice', 'custom-pds.com'],
['alice', 'my-custom-pds-with-long-name.social'],
['123456789012345678', 'my-custom-pds-with-long-name.social'],
]
it.each(valid)(`should be valid: %s.%s`, (handle, service) => {
const result = validateServiceHandle(handle, service)
expect(result.overall).toEqual(true)
})
const invalid = [
['al', 'bsky.social', 'frontLength'],
['-alice', 'bsky.social', 'hyphenStartOrEnd'],
['alice-', 'bsky.social', 'hyphenStartOrEnd'],
['%%%', 'bsky.social', 'handleChars'],
['1234567890123456789', 'bsky.social', 'frontLength'],
[
'1234567890123456789',
'my-custom-pds-with-long-name.social',
'frontLength',
],
['al', 'my-custom-pds-with-long-name.social', 'frontLength'],
['a'.repeat(300), 'toolong.com', 'totalLength'],
] satisfies [string, string, keyof IsValidHandle][]
it.each(invalid)(
`should be invalid: %s.%s due to %s`,
(handle, service, expectedError) => {
const result = validateServiceHandle(handle, service)
expect(result.overall).toEqual(false)
expect(result[expectedError]).toEqual(false)
},
)
})
+3 -33
View File
@@ -50,6 +50,7 @@ module.exports = function (config) {
runtimeVersion: {
policy: 'appVersion',
},
orientation: 'portrait',
icon: './assets/app-icons/ios_icon_default_light.png',
userInterfaceStyle: 'automatic',
primaryColor: '#1083fe',
@@ -75,38 +76,27 @@ module.exports = function (config) {
'an',
'ast',
'ca',
'cy',
'da',
'de',
'el',
'eo',
'es',
'eu',
'fi',
'fr',
'ga',
'gd',
'gl',
'hi',
'hu',
'ia',
'id',
'it',
'ja',
'km',
'ko',
'ne',
'nl',
'pl',
'pt-BR',
'ro',
'ru',
'sv',
'th',
'tr',
'uk',
'vi',
'yue',
'yue-Hant',
'zh-Hans',
'zh-Hant',
],
@@ -198,7 +188,6 @@ module.exports = function (config) {
channel: UPDATES_CHANNEL,
},
plugins: [
'expo-video',
'expo-localization',
USE_SENTRY && [
'@sentry/react-native/expo',
@@ -233,20 +222,12 @@ module.exports = function (config) {
},
],
'react-native-compressor',
[
'@bitdrift/react-native',
{
networkInstrumentation: true,
},
],
'@bitdrift/react-native',
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withGradleJVMHeapSizeIncrease.js',
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
'./plugins/withAndroidNoJitpackPlugin.js',
'./plugins/withNoBundleCompression.js',
'./plugins/shareExtension/withShareExtensions.js',
'./plugins/notificationsExtension/withNotificationsExtension.js',
'./plugins/withAppDelegateReferrer.js',
@@ -360,17 +341,6 @@ module.exports = function (config) {
},
},
],
['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
[
'react-native-vision-camera',
{
enableLocation: false,
cameraPermissionText: 'Bluesky needs access to your camera.',
enableMicrophonePermission: true,
microphonePermissionText:
'Bluesky needs access to your microphone.',
},
],
].filter(Boolean),
extra: {
eas: {
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.293 4.293a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1-1.414 1.414L12 6.414 8.707 9.707a1 1 0 0 1-1.414-1.414l4-4Zm-4 10a1 1 0 0 1 1.414 0L12 17.586l3.293-3.293a1 1 0 0 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 363 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm6.293-3.707a1 1 0 0 1 1.414 0L12 10.586l2.293-2.293a1 1 0 1 1 1.414 1.414L13.414 12l2.293 2.293a1 1 0 0 1-1.414 1.414L12 13.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L10.586 12 8.293 9.707a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 454 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7 2a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0V3a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1ZM4 9a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2h.5a3.5 3.5 0 1 1 0 7H18v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9Zm12 0H6v11h10V9Zm2 5h.5a1.5 1.5 0 0 0 0-3H18v3Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 437 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M6.5 4.999Zm0 0a.1.1 0 0 1-.016.024.2.2 0 0 1-.152.043q.086.014.168.032V7.5a1 1 0 0 1-.33.742c-.543.49-.917 1.176-1.23 2.036a1 1 0 0 1-.94.657H3V14h1a1 1 0 0 1 .866.5c.436.754.879 1.195 1.637 1.636A1 1 0 0 1 7 17v2h2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2h2v-2.537a1 1 0 0 1 .332-.744A4.98 4.98 0 0 0 19 11.999 5 5 0 0 0 14 7H9.474a1 1 0 0 1-.893-.55v-.001l-.001-.002-.005-.008L9.474 6l-.9.438h.001v.002l.002.001.001.003.002.003v.003a2.6 2.6 0 0 0-.443-.538c-.319-.298-.839-.648-1.637-.814V5Zm2.082 1.452ZM9.5 4.447c.211.196.379.387.508.553H14a7 7 0 0 1 6.72 8.96 1.003 1.003 0 0 0 1.146-1.46 1 1 0 1 1 1.731-1 3 3 0 0 1-3.714 4.286A7 7 0 0 1 19 16.888V19a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2h-2a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-1.45A5.8 5.8 0 0 1 3.448 16H3a2 2 0 0 1-2-2v-3.065a2 2 0 0 1 2-2h.324c.286-.649.657-1.291 1.176-1.852V5c0-1.047.89-2.12 2.161-1.907 1.33.223 2.248.805 2.839 1.354ZM8.25 12a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4c0-1.012.894-2.187 2.237-1.846a5.002 5.002 0 0 1 3.218 7.119 4 4 0 0 1 2.345 4.976A3.501 3.501 0 0 1 18.5 21h-13a3.5 3.5 0 0 1-1.3-6.75 4 4 0 0 1 2.052-4.848A4 4 0 0 1 10 4h2.001Zm-4.187 7.009A2 2 0 0 0 6 13c0 .366.098.706.271 1H12a1 1 0 1 1 0 2H5.5a1.5 1.5 0 0 0 0 3h13a1.5 1.5 0 1 0 0-3H17a1 1 0 1 1 0-2h.729A1.96 1.96 0 0 0 18 13a2 2 0 0 0-2-2h-2a1 1 0 1 1 0-2h1.236a3.002 3.002 0 0 0-1.243-4.832A2 2 0 0 1 12 6h-2a2 2 0 0 0-1.728 3.009H9a1 1 0 0 1 0 2H7.813Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 610 B

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 0-1 1v4.17a1 1 0 0 0 .667.944 2.001 2.001 0 0 1 0 3.772A1 1 0 0 0 2 14.83V19a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1v-4.17a1 1 0 0 0-.667-.944 2.001 2.001 0 0 1 0-3.772A1 1 0 0 0 22 9.17V5a1 1 0 0 0-1-1H3Zm1 4.535V6h16v2.535A4 4 0 0 0 18 12c0 1.482.805 2.773 2 3.465V18H4v-2.535A4 4 0 0 0 6 12a4 4 0 0 0-2-3.465ZM15 15a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-1-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0Zm1-5a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z" clip-rule="evenodd"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" stroke="#000" stroke-linejoin="round" d="M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.5 3.5 0 0 1 5.5 12a3.5 3.5 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.5 3.5 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z"/></svg>

Before

Width:  |  Height:  |  Size: 561 B

After

Width:  |  Height:  |  Size: 756 B

-1
View File
@@ -4,7 +4,6 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bluesky Embed</title>
<link rel="preconnect" href="https://public.api.bsky.app">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
-1
View File
@@ -4,7 +4,6 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bluesky Embed</title>
<link rel="preconnect" href="https://public.api.bsky.app">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
-1
View File
@@ -68,7 +68,6 @@ function scan(node = document) {
if (ref_url.startsWith('http')) {
searchParams.set('ref_url', encodeURIComponent(ref_url))
}
searchParams.set('colorMode', embed.dataset.blueskyColorMode || 'system')
const iframe = document.createElement('iframe')
iframe.setAttribute('data-bluesky-id', id)
+1 -7
View File
@@ -1,15 +1,9 @@
export type ColorModeValues = 'system' | 'light' | 'dark'
export function assertColorModeValues(value: string): value is ColorModeValues {
return ['system', 'light', 'dark'].includes(value)
}
export function applyTheme(theme: 'light' | 'dark') {
document.documentElement.classList.remove('light', 'dark')
document.documentElement.classList.add(theme)
}
export function initSystemColorMode() {
export function initColorMode() {
applyTheme(
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
-11
View File
@@ -9,14 +9,3 @@
:root {
color-scheme: light dark;
}
select {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='14px' width='14px' fill='none' viewBox='0 0 24 24'><path fill='black' fill-rule='evenodd' d='M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z' clip-rule='evenodd'/></svg>");
background-repeat: no-repeat;
background-position: calc(100% - 0.75rem) center;
padding-right: 2rem;
@media (prefers-color-scheme: dark) {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='14px' width='14px' fill='none' viewBox='0 0 24 24'><path fill='white' fill-rule='evenodd' d='M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z' clip-rule='evenodd'/></svg>");
}
}
-1
View File
@@ -13,7 +13,6 @@ export function labelsToInfo(
return 'Adult Content'
case 'nudity':
return 'Non-sexual Nudity'
case 'gore':
case 'graphic-media':
return 'Graphic Media'
default:
+19 -58
View File
@@ -1,16 +1,12 @@
import '../index.css'
import {AppBskyFeedDefs, AppBskyFeedPost, AtpAgent, AtUri} from '@atproto/api'
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri, BskyAgent} from '@atproto/api'
import {h, render} from 'preact'
import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import {
assertColorModeValues,
ColorModeValues,
initSystemColorMode,
} from '../color-mode'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
@@ -26,9 +22,9 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
initSystemColorMode()
initColorMode()
const agent = new AtpAgent({
const agent = new BskyAgent({
service: 'https://public.api.bsky.app',
})
@@ -36,7 +32,6 @@ render(<LandingPage />, root)
function LandingPage() {
const [uri, setUri] = useState('')
const [colorMode, setColorMode] = useState<ColorModeValues>('system')
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
@@ -125,50 +120,24 @@ function LandingPage() {
<h1 className="text-4xl font-bold text-center">Embed a Bluesky Post</h1>
<div className="flex flex-col w-full max-w-[600px] gap-6">
<input
type="text"
value={uri}
onInput={e => setUri(e.currentTarget.value)}
className="border rounded-lg py-3 px-4 dark:bg-dimmedBg dark:border-slate-500"
placeholder={DEFAULT_POST}
/>
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium" for="colorModeSelect">
Theme
</label>
<select
value={colorMode}
onChange={e => {
const value = e.currentTarget.value
if (assertColorModeValues(value)) {
setColorMode(value)
}
}}
id="colorModeSelect"
className="appearance-none bg-white border w-full rounded-lg text-sm px-3 py-2 dark:bg-dimmedBg dark:border-slate-500">
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</div>
<input
type="text"
value={uri}
onInput={e => setUri(e.currentTarget.value)}
className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500"
placeholder={DEFAULT_POST}
/>
<img src={arrowBottom} className="w-6 dark:invert" />
{loading ? (
<div className={`${colorMode} w-full max-w-[600px]`}>
<div className="w-full max-w-[600px]">
<Skeleton />
</div>
) : (
<div className="w-full max-w-[600px] gap-8 flex flex-col">
{!error && thread && uri && (
<Snippet thread={thread} colorMode={colorMode} />
)}
<div className={colorMode}>
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
</div>
{!error && thread && uri && <Snippet thread={thread} />}
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
{error && (
<div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg">
<p className="text-red-500 text-center">{error}</p>
@@ -199,13 +168,7 @@ function Skeleton() {
)
}
function Snippet({
thread,
colorMode,
}: {
thread: AppBskyFeedDefs.ThreadViewPost
colorMode: ColorModeValues
}) {
function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
const ref = useRef<HTMLInputElement>(null)
const [copied, setCopied] = useState(false)
@@ -241,11 +204,9 @@ function Snippet({
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
thread.post.uri,
)}" data-bluesky-cid="${escapeHtml(
thread.post.cid,
)}" data-bluesky-embed-color-mode="${escapeHtml(
colorMode,
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
)}" data-bluesky-cid="${escapeHtml(thread.post.cid)}"><p lang="${escapeHtml(
lang,
)}">${escapeHtml(record.text)}${
record.embed
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
: ''
@@ -256,7 +217,7 @@ function Snippet({
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
niceDate(thread.post.indexedAt),
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
}, [thread, colorMode])
}, [thread])
return (
<div className="flex gap-2 w-full">
+2 -18
View File
@@ -4,7 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
import {h, render} from 'preact'
import logo from '../../assets/logo.svg'
import {applyTheme, initSystemColorMode} from '../color-mode'
import {initColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
@@ -22,23 +22,7 @@ if (!uri) {
throw new Error('No uri in path')
}
const query = new URLSearchParams(window.location.search)
// theme - default to light mode
const colorMode = query.get('colorMode')
switch (colorMode) {
case 'dark':
applyTheme('dark')
break
case 'system':
initSystemColorMode()
break
case 'light':
default:
applyTheme('light')
break
}
initColorMode()
agent
.getPostThread({
+3 -5
View File
@@ -1,10 +1,9 @@
import {Express} from 'express'
import {AppContext} from '../context.js'
import {default as createShortLink} from './createShortLink.js'
import {default as create} from './create.js'
import {default as health} from './health.js'
import {default as redirect} from './redirect.js'
import {default as shortLink} from './shortLink.js'
import {default as siteAssociation} from './siteAssociation.js'
export * from './util.js'
@@ -12,8 +11,7 @@ export * from './util.js'
export default function (ctx: AppContext, app: Express) {
app = health(ctx, app) // GET /_health
app = siteAssociation(ctx, app) // GET /.well-known/apple-app-site-association
app = redirect(ctx, app) // GET /redirect
app = createShortLink(ctx, app) // POST /link
app = shortLink(ctx, app) // GET /:linkId (should go last due to permissive matching)
app = create(ctx, app) // POST /link
app = redirect(ctx, app) // GET /:linkId (should go last due to permissive matching)
return app
}
+31 -24
View File
@@ -6,40 +6,47 @@ import {Express} from 'express'
import {AppContext} from '../context.js'
import {handler} from './util.js'
const INTERNAL_IP_REGEX = new RegExp(
'(^127.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^10.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.1[6-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.2[0-9]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^172.3[0-1]{1}[0-9]{0,1}.[0-9]{1,3}.[0-9]{1,3}$)|(^192.168.[0-9]{1,3}.[0-9]{1,3}$)|^localhost',
'i',
)
export default function (ctx: AppContext, app: Express) {
return app.get(
'/redirect',
'/:linkId',
handler(async (req, res) => {
let link = req.query.u
const linkId = req.params.linkId
const contentType = req.accepts(['html', 'json'])
assert(
typeof link === 'string',
'express guarantees link query parameter is a string',
typeof linkId === 'string',
'express guarantees id parameter is a string',
)
link = decodeURIComponent(link)
let url: URL | undefined
try {
url = new URL(link)
} catch {}
if (
!url ||
(url.protocol !== 'http:' && url.protocol !== 'https:') || // is a http(s) url
(ctx.cfg.service.hostnames.includes(url.hostname.toLowerCase()) &&
url.pathname === '/redirect') || // is a redirect loop
INTERNAL_IP_REGEX.test(url.hostname) // isn't directing to an internal location
) {
const found = await ctx.db.db
.selectFrom('link')
.selectAll()
.where('id', '=', linkId)
.executeTakeFirst()
if (!found) {
// potentially broken or mistyped link
res.setHeader('Cache-Control', 'no-store')
if (contentType === 'json') {
return res
.status(404)
.json({
error: 'NotFound',
message: 'Link not found',
})
.end()
}
// send the user to the app
res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
return res.status(302).end()
}
// build url from original url in order to preserve query params
const url = new URL(
req.originalUrl,
`https://${ctx.cfg.service.appHostname}`,
)
url.pathname = found.path
res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
if (contentType === 'json') {
return res.json({url: url.href}).end()
}
res.setHeader('Location', url.href)
return res.status(301).end()
}),
-54
View File
@@ -1,54 +0,0 @@
import assert from 'node:assert'
import {DAY, SECOND} from '@atproto/common'
import {Express} from 'express'
import {AppContext} from '../context.js'
import {handler} from './util.js'
export default function (ctx: AppContext, app: Express) {
return app.get(
'/:linkId',
handler(async (req, res) => {
const linkId = req.params.linkId
const contentType = req.accepts(['html', 'json'])
assert(
typeof linkId === 'string',
'express guarantees id parameter is a string',
)
const found = await ctx.db.db
.selectFrom('link')
.selectAll()
.where('id', '=', linkId)
.executeTakeFirst()
if (!found) {
// potentially broken or mistyped link
res.setHeader('Cache-Control', 'no-store')
if (contentType === 'json') {
return res
.status(404)
.json({
error: 'NotFound',
message: 'Link not found',
})
.end()
}
// send the user to the app
res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
return res.status(302).end()
}
// build url from original url in order to preserve query params
const url = new URL(
req.originalUrl,
`https://${ctx.cfg.service.appHostname}`,
)
url.pathname = found.path
res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
if (contentType === 'json') {
return res.json({url: url.href}).end()
}
res.setHeader('Location', url.href)
return res.status(301).end()
}),
)
}
-1
View File
@@ -97,7 +97,6 @@ const hideAvatarLabels = new Set([
'nudity',
'sexual-figurative',
'graphic-media',
'gore',
'self-harm',
'sensitive',
'security',
+1 -1
View File
@@ -24,7 +24,7 @@ Then build and copy over the big 'ol `bundle.web.js` file:
### Golang Daemon
Install golang. We generally develop against the current stable release of the language, as declared in `go.mod`.
Install golang. We are generally using v1.22+.
In this directory (`bskyweb/`):
-14
View File
@@ -94,20 +94,6 @@ func run(args []string) {
Value: "",
EnvVars: []string{"STATIC_CDN_HOST"},
},
&cli.BoolFlag{
Name: "bsky-canonical-instance",
Usage: "Enable if this is the canonical deployment (bsky.app)",
Value: false,
Required: false,
EnvVars: []string{"BSKY_CANONICAL_INSTANCE"},
},
&cli.BoolFlag{
Name: "robots-disallow-all",
Usage: "Crawling is allowed by default. Enable this flag to Disallow all",
Value: false,
Required: false,
EnvVars: []string{"ROBOTS_DISALLOW_ALL"},
},
},
},
}
+14 -34
View File
@@ -63,8 +63,6 @@ func serve(cctx *cli.Context) error {
corsOrigins := cctx.StringSlice("cors-allowed-origins")
staticCDNHost := cctx.String("static-cdn-host")
staticCDNHost = strings.TrimSuffix(staticCDNHost, "/")
canonicalInstance := cctx.Bool("bsky-canonical-instance")
robotsDisallowAll := cctx.Bool("robots-disallow-all")
// Echo
e := echo.New()
@@ -206,41 +204,25 @@ func serve(cctx *cli.Context) error {
return http.FS(fsys)
}())
// enable some special endpoints for the "canonical" deployment (bsky.app). not having these enabled should *not* impact regular operation
if canonicalInstance {
e.GET("/ips-v4", echo.WrapHandler(staticHandler))
e.GET("/ips-v6", echo.WrapHandler(staticHandler))
e.GET("/security.txt", func(c echo.Context) error {
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
})
e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
}
// default to permissive, but Disallow all if flag set
if robotsDisallowAll {
e.File("/robots.txt", "static/robots-disallow-all.txt")
} else {
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
}
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
e.GET("/ips-v4", echo.WrapHandler(staticHandler))
e.GET("/ips-v6", echo.WrapHandler(staticHandler))
e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
e.GET("/security.txt", func(c echo.Context) error {
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
})
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Before(func() {
if c.Response().Status >= 300 {
return
}
path := c.Request().URL.Path
maxAge := 1 * (60 * 60) // default is 1 hour
path := c.Request().URL.Path
maxAge := 1 * (60 * 60) // default is 1 hour
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
maxAge = 365 * (60 * 60 * 24) // 1 year
}
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
maxAge = 365 * (60 * 60 * 24) // 1 year
}
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
})
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
return next(c)
}
})
@@ -253,7 +235,6 @@ func serve(cctx *cli.Context) error {
// generic routes
e.GET("/hashtag/:tag", server.WebGeneric)
e.GET("/topic/:topic", server.WebGeneric)
e.GET("/search", server.WebGeneric)
e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric)
@@ -295,7 +276,6 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
e.GET("/profile/:handleOrDID/search", server.WebGeneric)
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
+2 -2
View File
@@ -1,8 +1,8 @@
module github.com/bluesky-social/social-app/bskyweb
go 1.23
go 1.22
toolchain go1.23.4
toolchain go1.22.4
require (
github.com/bluesky-social/indigo v0.0.0-20240624223826-fd66f93ce141
-3
View File
@@ -1,3 +0,0 @@
# This is an development or self-hosted instance of the bsky web app, and crawling has been disallowed by the operator team.
User-Agent: *
Disallow: /
+2 -1
View File
@@ -8,11 +8,12 @@
Preconnect to essential domains
-->
<link rel="preconnect" href="https://bsky.social">
<link rel="preconnect" href="https://bsky.network">
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
<!-- Hello Humans! API docs at https://atproto.com -->
<link rel="preload" as="font" type="font/woff2" href="{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2" crossorigin>
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2">
<style>
/**
-21
View File
@@ -1,21 +0,0 @@
{
"project_id": "1",
"base_url": "https://bluesky.api.crowdin.com",
"base_path": ".",
"preserve_hierarchy": true,
"files": [
{
"source": "/src/locale/locales/en/messages.po",
"translation": "/src/locale/locales/%two_letters_code%/messages.po",
"languages_mapping": {
"two_letters_code": {
"pt-BR": "pt-BR",
"en-GB": "en-GB",
"zh-CN": "zh-CN",
"zh-TW": "zh-TW",
"zh-HK": "zh-HK"
}
}
}
]
}
+8 -59
View File
@@ -1,67 +1,11 @@
# Internationalization
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please get involved on [Crowdin](https://bluesky.crowdin.com/u/projects/1) or open an issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app).
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please open a PR or issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app)
## Tools
- We use Crowdin to manage translations.
- Bluesky Crowdin: https://bluesky.crowdin.com/u/projects/1
- Introduction to Crowdin: https://support.crowdin.com/for-translators/
- We use Lingui to implement translations. You can find the documentation [here](https://lingui.dev/).
## Translators
Much of the app is translated by community contributions. (We <3 our translators!) If you want to participate in the translation of the app, read this section.
### Using Crowdin
[Crowdin](https://bluesky.crowdin.com/u/projects/1) is our primary tool for managing translations. There are two roles:
- **Proof-readers**. Can create new translations and approve submitted translations.
- **Translators**. Can create new translations.
All translations must be approved by proof-readers before they are accepted into the app.
### Using other platforms
You may contribute PRs separately from Crowdin, however we strongly recommend using Crowdin to avoid conflicts.
### Code of conduct on Crowdin
Please treat everyone with respect. Proof-readers are given final say on translations. Translators who frequently come into conflict with other translators, or who contribute noticably incorrect translations, will have their membership to the Crowdin project revoked.
### Adding a new language
You can request a new language be added to the project by clicking **Request New Language** on Crowdin or you can create a [GitHub issue](https://github.com/bluesky-social/social-app/issues).
Please only request a new language when you are certain you will be able to contribute a substantive portion of translations for the language.
## Maintainers
Install the [Crowdin CLI](https://crowdin.github.io/crowdin-cli/). You will need to [configure your API token](https://crowdin.github.io/crowdin-cli/configuration) to access the project.
### English source-file sync with Crowdin
Every night, a GitHub action will run `yarn intl:extract` to update the english `messages.po` file. This will be automatically synced with Crowdin. Crowdin should notify all subscribed users of new translations.
### Release process
1. Pull main and create a branch.
1. Run `yarn intl:pull` to fetch all translation updates from Crowdin. Commit.
1. Run `yarn intl:extract:all` to ensure all `.po` files are synced with the current state of the code. Commit.
1. Create a PR, ensure the translations all look correct, and merge.
1. If needed:
1. Merge all approved translation PRs (contributions from outside crowdin).
1. Run `yarn intl:push` to sync Crowdin with the state of the repo.
### Testing the translations in Crowdin
You can run `yarn intl:pull` to pull the currently-approved translations from Crowdin.
## Developers
We are using Lingui to manage translations. You can find the documentation [here](https://lingui.dev/).
### Adding new strings
When adding a new string, do it as follows:
```jsx
// Before
@@ -117,7 +61,6 @@ So the workflow is as follows:
6. Enjoy translated app!
### Common pitfalls
These pitfalls are memoization pitfalls that will cause the components to not re-render when the locale is changed -- causing stale translations to be shown.
```jsx
@@ -168,3 +111,9 @@ export function Welcome() {
return <div>{welcome}</div>;
}
```
### Credits
Please check each individual `messages.po` file for the credits of the translators. We are very grateful for their help!
If you would like to translate the Bluesky app into your language, please open a PR or issue on this repo.
+1 -1
View File
@@ -119,5 +119,5 @@ jest.mock('statsig-react-native-expo', () => ({
},
}))
jest.mock('../src/logger/bitdrift/lib', () => ({}))
jest.mock('../src/lib/bitdrift', () => ({}))
jest.mock('../src/lib/statsig/statsig', () => ({}))
-11
View File
@@ -5,34 +5,23 @@ module.exports = {
'an',
'ast',
'ca',
'cy',
'da',
'de',
'el',
'en-GB',
'eo',
'es',
'eu',
'fi',
'fr',
'ga',
'gd',
'gl',
'hi',
'hu',
'ia',
'id',
'it',
'ja',
'km',
'ko',
'ne',
'nl',
'pl',
'pt-BR',
'ro',
'ru',
'sv',
'th',
'tr',
'uk',
@@ -31,13 +31,9 @@ class BottomSheetView(
private lateinit var dialogRootViewGroup: DialogRootViewGroup
private var eventDispatcher: EventDispatcher? = null
private val rawScreenHeight = context.resources.displayMetrics.heightPixels.toFloat()
private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).toFloat()
private fun getNavigationBarHeight(): Int {
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
}
private val screenHeight =
context.resources.displayMetrics.heightPixels
.toFloat()
private val onAttemptDismiss by EventDispatcher()
private val onSnapPointChange by EventDispatcher()
@@ -67,12 +63,12 @@ class BottomSheetView(
}
}
var maxHeight = this.safeScreenHeight
var maxHeight = this.screenHeight
set(value) {
val px = dpToPx(value)
field =
if (px > this.safeScreenHeight) {
this.safeScreenHeight
if (px > this.screenHeight) {
this.screenHeight
} else {
px
}
@@ -157,19 +153,6 @@ class BottomSheetView(
// Presentation
private fun getHalfExpandedRatio(contentHeight: Float): Float {
return when {
// Full height sheets
contentHeight >= safeScreenHeight -> 0.99f
// Medium height sheets (>50% but <100%)
contentHeight >= safeScreenHeight / 2 ->
this.clampRatio(this.getTargetHeight() / safeScreenHeight)
// Small height sheets (<50%)
else ->
this.clampRatio(this.getTargetHeight() / rawScreenHeight)
}
}
private fun present() {
if (this.isOpen || this.isOpening || this.isClosing) return
@@ -189,12 +172,12 @@ class BottomSheetView(
val behavior = BottomSheetBehavior.from(it)
behavior.state = BottomSheetBehavior.STATE_HIDDEN
behavior.isFitToContents = true
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
behavior.skipCollapsed = true
behavior.isDraggable = true
behavior.isHideable = true
if (contentHeight >= this.safeScreenHeight || this.minHeight >= this.safeScreenHeight) {
if (contentHeight >= this.screenHeight || this.minHeight >= this.screenHeight) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
this.selectedSnapPoint = 2
} else {
@@ -244,11 +227,11 @@ class BottomSheetView(
bottomSheet?.let {
val behavior = BottomSheetBehavior.from(it)
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
if (contentHeight > this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
if (contentHeight > this.screenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
} else if (contentHeight < this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
} else if (contentHeight < this.screenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
}
@@ -1,17 +1,14 @@
import * as React from 'react'
import {
Dimensions,
LayoutChangeEvent,
NativeSyntheticEvent,
Platform,
StyleProp,
View,
ViewStyle,
} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
import {isIOS} from '#/platform/detection'
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
import {BottomSheetPortalProvider} from './BottomSheetPortal'
import {Context as PortalContext} from './BottomSheetPortal'
@@ -84,10 +81,12 @@ export class BottomSheetNativeComponent extends React.Component<
return null
}
const {children, backgroundColor, ...rest} = this.props
const cornerRadius = rest.cornerRadius ?? 0
let extraStyles
if (isIOS15 && this.state.viewHeight) {
const {viewHeight} = this.state
const cornerRadius = this.props.cornerRadius ?? 0
if (viewHeight < screenHeight / 2) {
extraStyles = {
height: viewHeight,
@@ -100,70 +99,39 @@ export class BottomSheetNativeComponent extends React.Component<
return (
<Portal>
<BottomSheetNativeComponentInner
{...this.props}
nativeViewRef={this.ref}
<NativeView
{...rest}
onStateChange={this.onStateChange}
extraStyles={extraStyles}
onLayout={e => {
const {height} = e.nativeEvent.layout
this.setState({viewHeight: height})
this.updateLayout()
ref={this.ref}
style={{
position: 'absolute',
height: screenHeight,
width: '100%',
}}
/>
containerBackgroundColor={backgroundColor}>
<View
style={[
{
flex: 1,
backgroundColor,
},
Platform.OS === 'android' && {
borderTopLeftRadius: cornerRadius,
borderTopRightRadius: cornerRadius,
},
extraStyles,
]}>
<View
onLayout={e => {
const {height} = e.nativeEvent.layout
this.setState({viewHeight: height})
this.updateLayout()
}}>
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
</View>
</View>
</NativeView>
</Portal>
)
}
}
function BottomSheetNativeComponentInner({
children,
backgroundColor,
onLayout,
onStateChange,
nativeViewRef,
extraStyles,
...rest
}: BottomSheetViewProps & {
extraStyles?: StyleProp<ViewStyle>
onStateChange: (
event: NativeSyntheticEvent<{state: BottomSheetState}>,
) => void
nativeViewRef: React.RefObject<View>
onLayout: (event: LayoutChangeEvent) => void
}) {
const insets = useSafeAreaInsets()
const cornerRadius = rest.cornerRadius ?? 0
const sheetHeight = isIOS ? screenHeight - insets.top : screenHeight
return (
<NativeView
{...rest}
onStateChange={onStateChange}
ref={nativeViewRef}
style={{
position: 'absolute',
height: sheetHeight,
width: '100%',
}}
containerBackgroundColor={backgroundColor}>
<View
style={[
{
flex: 1,
backgroundColor,
},
Platform.OS === 'android' && {
borderTopLeftRadius: cornerRadius,
borderTopRightRadius: cornerRadius,
},
extraStyles,
]}>
<View onLayout={onLayout}>
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
</View>
</View>
</NativeView>
)
}
+17 -45
View File
@@ -1,11 +1,11 @@
{
"name": "bsky.app",
"version": "1.99.1",
"version": "1.96.0",
"private": true,
"engines": {
"node": ">=20"
},
"packageManager": "yarn@1.22.22",
"packageManager": "yarn@1.22.19",
"scripts": {
"prepare": "is-ci || husky install",
"postinstall": "patch-package && yarn intl:compile",
@@ -42,12 +42,9 @@
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand \"yarn perf:test\" --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure",
"intl:build": "yarn intl:extract:all && yarn intl:compile",
"intl:extract": "lingui extract --clean --locale en",
"intl:extract:all": "lingui extract --clean",
"intl:build": "yarn intl:extract && yarn intl:compile",
"intl:extract": "lingui extract",
"intl:compile": "lingui compile",
"intl:pull": "crowdin download translations --verbose -b main",
"intl:push": "crowdin push translations --verbose -b main",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
"update-extensions": "bash scripts/updateExtensions.sh",
"export": "npx expo export",
@@ -57,8 +54,8 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.14.7",
"@bitdrift/react-native": "^0.6.8",
"@atproto/api": "^0.13.21",
"@bitdrift/react-native": "0.4.0",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1",
@@ -66,6 +63,7 @@
"@expo/webpack-config": "^19.0.0",
"@floating-ui/dom": "^1.6.3",
"@floating-ui/react-dom": "^2.0.8",
"@formatjs/intl-datetimeformat": "^6.17.1",
"@formatjs/intl-locale": "^4.2.8",
"@formatjs/intl-numberformat": "^8.15.1",
"@formatjs/intl-pluralrules": "^5.4.1",
@@ -73,16 +71,16 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.2",
"@haileyok/bluesky-video": "0.2.6",
"@haileyok/bluesky-video": "0.2.4",
"@ipld/dag-cbor": "^9.2.0",
"@lingui/react": "^4.14.1",
"@mattermost/react-native-paste-input": "^0.7.1",
"@miblanchard/react-native-slider": "^2.3.1",
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
"@radix-ui/react-dismissable-layer": "^1.1.5",
"@radix-ui/react-dropdown-menu": "2.1.6",
"@radix-ui/react-dismissable-layer": "^1.1.1",
"@radix-ui/react-dropdown-menu": "2.0.1",
"@radix-ui/react-focus-guards": "^1.1.1",
"@radix-ui/react-focus-scope": "^1.1.2",
"@radix-ui/react-focus-scope": "^1.1.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-menu/menu": "^1.1.7",
"@react-native-picker/picker": "2.10.2",
@@ -124,7 +122,7 @@
"eventemitter3": "^5.0.1",
"expo": "~52.0.19",
"expo-application": "^6.0.1",
"expo-blur": "^14.0.3",
"expo-blur": "^14.0.1",
"expo-build-properties": "^0.13.1",
"expo-camera": "~16.0.7",
"expo-clipboard": "^7.0.0",
@@ -142,14 +140,12 @@
"expo-media-library": "~17.0.3",
"expo-navigation-bar": "~4.0.4",
"expo-notifications": "~0.29.11",
"expo-screen-orientation": "^8.0.2",
"expo-sharing": "^13.0.0",
"expo-splash-screen": "~0.29.18",
"expo-status-bar": "~2.0.0",
"expo-system-ui": "^4.0.4",
"expo-task-manager": "~12.0.3",
"expo-updates": "~0.26.10",
"expo-video": "^2.0.5",
"expo-web-browser": "~14.0.1",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
@@ -173,17 +169,17 @@
"react-dom": "^18.2.0",
"react-image-crop": "^11.0.7",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.76.6",
"react-native": "0.76.3",
"react-native-compressor": "1.10.3",
"react-native-date-picker": "^5.0.7",
"react-native-drawer-layout": "^4.1.1",
"react-native-drawer-layout": "^4.0.4",
"react-native-gesture-handler": "2.20.2",
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "^0.41.6",
"react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.14.5",
"react-native-mmkv": "^2.12.2",
"react-native-pager-view": "6.7.0",
"react-native-pager-view": "6.6.1",
"react-native-picker-select": "^9.3.1",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.3",
@@ -196,7 +192,6 @@
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.2",
"react-native-view-shot": "^4.0.3",
"react-native-vision-camera": "^4.6.3",
"react-native-web": "~0.19.13",
"react-native-web-webview": "^1.0.2",
"react-native-webview": "13.12.3",
@@ -212,7 +207,7 @@
"zod": "^3.20.2"
},
"devDependencies": {
"@atproto/dev-env": "^0.3.87",
"@atproto/dev-env": "^0.3.67",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
@@ -256,7 +251,6 @@
"jest-expo": "^52.0.2",
"jest-junit": "^16.0.0",
"lint-staged": "^13.2.3",
"lockfile-lint": "^4.14.0",
"metro-react-native-babel-preset": "^0.76.9",
"prettier": "^2.8.3",
"react-native-dotenv": "^3.4.11",
@@ -271,6 +265,7 @@
"@expo/image-utils": "0.6.3",
"@react-native/babel-preset": "0.76.1",
"@react-native/normalize-colors": "0.76.1",
"@radix-ui/react-focus-scope": "1.1.0",
"@types/react": "^18",
"**/expo-constants": "17.0.3",
"**/expo-device": "7.0.1",
@@ -335,28 +330,5 @@
"assets/icons/*.svg": [
"svgo"
]
},
"lockfile-lint": {
"path": "yarn.lock",
"allowedHosts": [
"npm",
"yarn"
],
"allowedSchemes": [
"https:"
],
"allowedPackageNameAliases": [
"@babel/traverse--for-generate-function-map:@babel/traverse",
"string-width-cjs:string-width",
"strip-ansi-cjs:strip-ansi",
"wrap-ansi-cjs:wrap-ansi"
],
"allowedUrls": [
"https://codeload.github.com/bluesky-social/react-native-bottom-sheet/tar.gz/28a87d1bb55e10fc355fa1455545a30734995908",
"https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4"
],
"emptyHostname": false,
"validatePackageNames": true,
"validateIntegrity": true
}
}
@@ -1,157 +1,16 @@
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
index e916023..5049c33 100644
index e916023..0564d97 100644
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
@@ -4,6 +4,7 @@
//
// Created by Elias Nahum on 04-11-20.
// Copyright © 2020 Facebook. All rights reserved.
+// Updated to remove parents default text view
//
#import "PasteInputView.h"
@@ -12,49 +13,78 @@
@implementation PasteInputView
{
- PasteInputTextView *_backedTextInputView;
+ // We'll store the custom text view in this ivar
+ PasteInputTextView *_customBackedTextView;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
+ // Must call the supers designated initializer
if (self = [super initWithBridge:bridge]) {
- _backedTextInputView = [[PasteInputTextView alloc] initWithFrame:self.bounds];
- _backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- _backedTextInputView.textInputDelegate = self;
+ // 1. The parent (RCTMultilineTextInputView) has already created
+ // its own _backedTextInputView = [RCTUITextView new] in super init.
+ // We can remove that subview:
- [self addSubview:_backedTextInputView];
- }
+ id<RCTBackedTextInputViewProtocol> parentInputView = super.backedTextInputView;
+ if ([parentInputView isKindOfClass:[UIView class]]) {
+ UIView *parentSubview = (UIView *)parentInputView;
+ if (parentSubview.superview == self) {
+ [parentSubview removeFromSuperview];
+ }
+ }
+ // 2. Now create our custom PasteInputTextView
+ _customBackedTextView = [[PasteInputTextView alloc] initWithFrame:self.bounds];
+ _customBackedTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ _customBackedTextView.textInputDelegate = self;
+
+ // Optional: disable inline predictions for iOS 17+
@@ -22,6 +22,11 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_backedTextInputView.textInputDelegate = self;
+ // Disable inline predictions to prevent jank in the composer
+ if (@available(iOS 17.0, *)) {
+ _customBackedTextView.inlinePredictionType = UITextInlinePredictionTypeNo;
+ _backedTextInputView.inlinePredictionType = UITextInlinePredictionTypeNo;
+ }
+
+ // 3. Add your custom text view as the only subview
+ [self addSubview:_customBackedTextView];
+ }
return self;
}
+/**
+ * Override the parent's accessor so that anywhere in RN that calls
+ * `self.backedTextInputView` will get the custom PasteInputTextView.
+ */
- (id<RCTBackedTextInputViewProtocol>)backedTextInputView
{
- return _backedTextInputView;
+ return _customBackedTextView;
}
-- (void)setDisableCopyPaste:(BOOL)disableCopyPaste {
- _backedTextInputView.disableCopyPaste = disableCopyPaste;
+#pragma mark - Setters for React Props
+
+- (void)setDisableCopyPaste:(BOOL)disableCopyPaste
+{
+ _customBackedTextView.disableCopyPaste = disableCopyPaste;
}
-- (void)setOnPaste:(RCTDirectEventBlock)onPaste {
- _backedTextInputView.onPaste = onPaste;
+- (void)setOnPaste:(RCTDirectEventBlock)onPaste
+{
+ _customBackedTextView.onPaste = onPaste;
}
-- (void)setSmartPunctuation:(NSString *)smartPunctuation {
- if ([smartPunctuation isEqualToString:@"enable"]) {
- [_backedTextInputView setSmartDashesType:UITextSmartDashesTypeYes];
- [_backedTextInputView setSmartQuotesType:UITextSmartQuotesTypeYes];
- [_backedTextInputView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeYes];
- } else if ([smartPunctuation isEqualToString:@"disable"]) {
- [_backedTextInputView setSmartDashesType:UITextSmartDashesTypeNo];
- [_backedTextInputView setSmartQuotesType:UITextSmartQuotesTypeNo];
- [_backedTextInputView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeNo];
- } else {
- [_backedTextInputView setSmartDashesType:UITextSmartDashesTypeDefault];
- [_backedTextInputView setSmartQuotesType:UITextSmartQuotesTypeDefault];
- [_backedTextInputView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeDefault];
- }
+- (void)setSmartPunctuation:(NSString *)smartPunctuation
+{
+ if ([smartPunctuation isEqualToString:@"enable"]) {
+ [_customBackedTextView setSmartDashesType:UITextSmartDashesTypeYes];
+ [_customBackedTextView setSmartQuotesType:UITextSmartQuotesTypeYes];
+ [_customBackedTextView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeYes];
+ } else if ([smartPunctuation isEqualToString:@"disable"]) {
+ [_customBackedTextView setSmartDashesType:UITextSmartDashesTypeNo];
+ [_customBackedTextView setSmartQuotesType:UITextSmartQuotesTypeNo];
+ [_customBackedTextView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeNo];
+ } else {
+ [_customBackedTextView setSmartDashesType:UITextSmartDashesTypeDefault];
+ [_customBackedTextView setSmartQuotesType:UITextSmartQuotesTypeDefault];
+ [_customBackedTextView setSmartInsertDeleteType:UITextSmartInsertDeleteTypeDefault];
+ }
}
#pragma mark - UIScrollViewDelegate
@@ -62,7 +92,6 @@
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
RCTDirectEventBlock onScroll = self.onScroll;
-
if (onScroll) {
CGPoint contentOffset = scrollView.contentOffset;
CGSize contentSize = scrollView.contentSize;
@@ -71,22 +100,22 @@
onScroll(@{
@"contentOffset": @{
- @"x": @(contentOffset.x),
- @"y": @(contentOffset.y)
+ @"x": @(contentOffset.x),
+ @"y": @(contentOffset.y)
},
@"contentInset": @{
- @"top": @(contentInset.top),
- @"left": @(contentInset.left),
- @"bottom": @(contentInset.bottom),
- @"right": @(contentInset.right)
+ @"top": @(contentInset.top),
+ @"left": @(contentInset.left),
+ @"bottom": @(contentInset.bottom),
+ @"right": @(contentInset.right)
},
@"contentSize": @{
- @"width": @(contentSize.width),
- @"height": @(contentSize.height)
+ @"width": @(contentSize.width),
+ @"height": @(contentSize.height)
},
@"layoutMeasurement": @{
- @"width": @(size.width),
- @"height": @(size.height)
+ @"width": @(size.width),
+ @"height": @(size.height)
},
@"zoomScale": @(scrollView.zoomScale ?: 1),
});
[self addSubview:_backedTextInputView];
}
-19
View File
@@ -1,19 +0,0 @@
diff --git a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
index b928b8f..7175cf6 100644
--- a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
+++ b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
@@ -99,12 +99,12 @@ class MediaLibraryModule : Module() {
}
AsyncFunction("createAssetAsync") { localUri: String, promise: Promise ->
- throwUnlessPermissionsGranted {
+ //throwUnlessPermissionsGranted {
withModuleScope(promise) {
CreateAsset(context, localUri, promise)
.execute()
}
- }
+ //}
}
AsyncFunction("addAssetsToAlbumAsync") { assetsId: List<String>, albumId: String, copyToAlbum: Boolean, promise: Promise ->
@@ -0,0 +1,876 @@
diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/index.js b/node_modules/react-native-drawer-layout/lib/commonjs/index.js
index 3dce76e..6c4b3e5 100644
--- a/node_modules/react-native-drawer-layout/lib/commonjs/index.js
+++ b/node_modules/react-native-drawer-layout/lib/commonjs/index.js
@@ -9,6 +9,12 @@ Object.defineProperty(exports, "Drawer", {
return _Drawer.Drawer;
}
});
+Object.defineProperty(exports, "DrawerGestureContext", {
+ enumerable: true,
+ get: function () {
+ return _DrawerGestureContext.DrawerGestureContext;
+ }
+});
Object.defineProperty(exports, "DrawerProgressContext", {
enumerable: true,
get: function () {
@@ -21,6 +27,7 @@ Object.defineProperty(exports, "useDrawerProgress", {
return _useDrawerProgress.useDrawerProgress;
}
});
+var _DrawerGestureContext = require("./utils/DrawerGestureContext.js");
var _DrawerProgressContext = require("./utils/DrawerProgressContext.js");
var _useDrawerProgress = require("./utils/useDrawerProgress.js");
var _Drawer = require("./views/Drawer");
diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js b/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js
new file mode 100644
index 0000000..de6d793
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/commonjs/utils/DrawerGestureContext.js
@@ -0,0 +1,11 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.DrawerGestureContext = void 0;
+var React = _interopRequireWildcard(require("react"));
+function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
+function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
+const DrawerGestureContext = exports.DrawerGestureContext = /*#__PURE__*/React.createContext(undefined);
+//# sourceMappingURL=DrawerGestureContext.js.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js b/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js
index fd65ab8..86a9a6a 100644
--- a/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js
+++ b/node_modules/react-native-drawer-layout/lib/commonjs/views/Drawer.native.js
@@ -8,6 +8,7 @@ var React = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
var _useLatestCallback = _interopRequireDefault(require("use-latest-callback"));
+var _DrawerGestureContext = require("../utils/DrawerGestureContext.js");
var _DrawerProgressContext = require("../utils/DrawerProgressContext.js");
var _getDrawerWidth = require("../utils/getDrawerWidth.js");
var _GestureHandler = require("./GestureHandler");
@@ -79,38 +80,38 @@ function Drawer({
return () => hideStatusBar(false);
}, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]);
const interactionHandleRef = React.useRef(null);
- const startInteraction = () => {
+ const startInteraction = React.useCallback(() => {
interactionHandleRef.current = _reactNative.InteractionManager.createInteractionHandle();
- };
- const endInteraction = () => {
+ }, []);
+ const endInteraction = React.useCallback(() => {
if (interactionHandleRef.current != null) {
_reactNative.InteractionManager.clearInteractionHandle(interactionHandleRef.current);
interactionHandleRef.current = null;
}
- };
- const hideKeyboard = () => {
+ }, []);
+ const hideKeyboard = React.useCallback(() => {
if (keyboardDismissMode === 'on-drag') {
_reactNative.Keyboard.dismiss();
}
- };
- const onGestureBegin = () => {
+ }, [keyboardDismissMode]);
+ const onGestureBegin = React.useCallback(() => {
onGestureStart?.();
startInteraction();
hideKeyboard();
hideStatusBar(true);
- };
- const onGestureFinish = () => {
+ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]);
+ const onGestureFinish = React.useCallback(() => {
onGestureEnd?.();
endInteraction();
- };
- const onGestureAbort = () => {
+ }, [onGestureEnd, endInteraction]);
+ const onGestureAbort = React.useCallback(() => {
onGestureCancel?.();
endInteraction();
- };
+ }, [onGestureCancel, endInteraction]);
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/software-mansion/react-native-gesture-handler/issues/569
- const hitSlop = isRight ?
+ const hitSlop = React.useMemo(() => isRight ?
// Extend hitSlop to the side of the screen when drawer is closed
// This lets the user drag the drawer from the side of the screen
{
@@ -119,7 +120,7 @@ function Drawer({
} : {
left: 0,
width: isOpen ? undefined : swipeEdgeWidth
- };
+ }, [isRight, isOpen, swipeEdgeWidth]);
const touchStartX = (0, _reactNativeReanimated.useSharedValue)(0);
const touchX = (0, _reactNativeReanimated.useSharedValue)(0);
const translationX = (0, _reactNativeReanimated.useSharedValue)(getDrawerTranslationX(open));
@@ -158,40 +159,43 @@ function Drawer({
}, [getDrawerTranslationX, handleAnimationEnd, handleAnimationStart, onClose, onOpen, touchStartX, touchX, translationX]);
React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
const startX = (0, _reactNativeReanimated.useSharedValue)(0);
- let pan = _GestureHandler.Gesture?.Pan().onBegin(event => {
- 'worklet';
+ const pan = React.useMemo(() => {
+ let panGesture = _GestureHandler.Gesture?.Pan().onBegin(event => {
+ 'worklet';
- startX.value = translationX.value;
- gestureState.value = event.state;
- touchStartX.value = event.x;
- }).onStart(() => {
- 'worklet';
+ startX.value = translationX.value;
+ gestureState.value = event.state;
+ touchStartX.value = event.x;
+ }).onStart(() => {
+ 'worklet';
- (0, _reactNativeReanimated.runOnJS)(onGestureBegin)();
- }).onChange(event => {
- 'worklet';
+ (0, _reactNativeReanimated.runOnJS)(onGestureBegin)();
+ }).onChange(event => {
+ 'worklet';
- touchX.value = event.x;
- translationX.value = startX.value + event.translationX;
- gestureState.value = event.state;
- }).onEnd((event, success) => {
- 'worklet';
+ touchX.value = event.x;
+ translationX.value = startX.value + event.translationX;
+ gestureState.value = event.state;
+ }).onEnd((event, success) => {
+ 'worklet';
- gestureState.value = event.state;
- if (!success) {
- (0, _reactNativeReanimated.runOnJS)(onGestureAbort)();
+ gestureState.value = event.state;
+ if (!success) {
+ (0, _reactNativeReanimated.runOnJS)(onGestureAbort)();
+ }
+ const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
+ // If swiped to right, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
+ // If swiped to left, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
+ toggleDrawer(nextOpen, event.velocityX);
+ (0, _reactNativeReanimated.runOnJS)(onGestureFinish)();
+ }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
+ if (panGesture && configureGestureHandler) {
+ panGesture = configureGestureHandler(panGesture);
}
- const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
- // If swiped to right, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
- // If swiped to left, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
- toggleDrawer(nextOpen, event.velocityX);
- (0, _reactNativeReanimated.runOnJS)(onGestureFinish)();
- }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
- if (pan && configureGestureHandler) {
- pan = configureGestureHandler(pan);
- }
+ return panGesture;
+ }, [configureGestureHandler, drawerPosition, drawerType, gestureState, hitSlop, onGestureBegin, onGestureAbort, onGestureFinish, open, startX, swipeEnabled, swipeMinDistance, swipeMinVelocity, toggleDrawer, touchStartX, touchX, translationX]);
const translateX = (0, _reactNativeReanimated.useDerivedValue)(() => {
// Comment stolen from react-native-gesture-handler/DrawerLayout
//
@@ -254,35 +258,38 @@ function Drawer({
style: [styles.container, style],
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DrawerProgressContext.DrawerProgressContext.Provider, {
value: progress,
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_GestureHandler.GestureDetector, {
- gesture: pan,
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
- style: [styles.main, {
- flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
- }],
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
- style: [styles.content, contentAnimatedStyle],
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
- accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
- importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
- style: styles.content,
- children: children
- }), drawerType !== 'permanent' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Overlay.Overlay, {
- open: open,
- progress: progress,
- onPress: () => toggleDrawer(false),
- style: overlayStyle,
- accessibilityLabel: overlayAccessibilityLabel
- }) : null]
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
- removeClippedSubviews: _reactNative.Platform.OS !== 'ios',
- style: [styles.drawer, {
- width: drawerWidth,
- position: drawerType === 'permanent' ? 'relative' : 'absolute',
- zIndex: drawerType === 'back' ? -1 : 0
- }, drawerAnimatedStyle, drawerStyle],
- children: renderDrawerContent()
- })]
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_DrawerGestureContext.DrawerGestureContext.Provider, {
+ value: pan,
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_GestureHandler.GestureDetector, {
+ gesture: pan,
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
+ style: [styles.main, {
+ flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
+ }],
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
+ style: [styles.content, contentAnimatedStyle],
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
+ accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
+ importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
+ style: styles.content,
+ children: children
+ }), drawerType !== 'permanent' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Overlay.Overlay, {
+ open: open,
+ progress: progress,
+ onPress: () => toggleDrawer(false),
+ style: overlayStyle,
+ accessibilityLabel: overlayAccessibilityLabel
+ }) : null]
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
+ removeClippedSubviews: _reactNative.Platform.OS !== 'ios',
+ style: [styles.drawer, {
+ width: drawerWidth,
+ position: drawerType === 'permanent' ? 'relative' : 'absolute',
+ zIndex: drawerType === 'back' ? -1 : 0
+ }, drawerAnimatedStyle, drawerStyle],
+ children: renderDrawerContent()
+ })]
+ })
})
})
})
diff --git a/node_modules/react-native-drawer-layout/lib/module/index.js b/node_modules/react-native-drawer-layout/lib/module/index.js
index a600e51..f08275c 100644
--- a/node_modules/react-native-drawer-layout/lib/module/index.js
+++ b/node_modules/react-native-drawer-layout/lib/module/index.js
@@ -1,5 +1,6 @@
"use strict";
+export { DrawerGestureContext } from "./utils/DrawerGestureContext.js";
export { DrawerProgressContext } from "./utils/DrawerProgressContext.js";
export { useDrawerProgress } from "./utils/useDrawerProgress.js";
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js b/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js
new file mode 100644
index 0000000..1adaa9c
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/module/utils/DrawerGestureContext.js
@@ -0,0 +1,5 @@
+"use strict";
+
+import * as React from 'react';
+export const DrawerGestureContext = /*#__PURE__*/React.createContext(undefined);
+//# sourceMappingURL=DrawerGestureContext.js.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js b/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
index 6d07126..981d9f8 100644
--- a/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
+++ b/node_modules/react-native-drawer-layout/lib/module/views/Drawer.native.js
@@ -4,6 +4,7 @@ import * as React from 'react';
import { I18nManager, InteractionManager, Keyboard, Platform, StatusBar, StyleSheet, useWindowDimensions, View } from 'react-native';
import Animated, { interpolate, ReduceMotion, runOnJS, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring } from 'react-native-reanimated';
import useLatestCallback from 'use-latest-callback';
+import { DrawerGestureContext } from "../utils/DrawerGestureContext.js";
import { DrawerProgressContext } from "../utils/DrawerProgressContext.js";
import { getDrawerWidth } from "../utils/getDrawerWidth.js";
import { Gesture, GestureDetector, GestureHandlerRootView, GestureState } from './GestureHandler';
@@ -72,38 +73,38 @@ export function Drawer({
return () => hideStatusBar(false);
}, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]);
const interactionHandleRef = React.useRef(null);
- const startInteraction = () => {
+ const startInteraction = React.useCallback(() => {
interactionHandleRef.current = InteractionManager.createInteractionHandle();
- };
- const endInteraction = () => {
+ }, []);
+ const endInteraction = React.useCallback(() => {
if (interactionHandleRef.current != null) {
InteractionManager.clearInteractionHandle(interactionHandleRef.current);
interactionHandleRef.current = null;
}
- };
- const hideKeyboard = () => {
+ }, []);
+ const hideKeyboard = React.useCallback(() => {
if (keyboardDismissMode === 'on-drag') {
Keyboard.dismiss();
}
- };
- const onGestureBegin = () => {
+ }, [keyboardDismissMode]);
+ const onGestureBegin = React.useCallback(() => {
onGestureStart?.();
startInteraction();
hideKeyboard();
hideStatusBar(true);
- };
- const onGestureFinish = () => {
+ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]);
+ const onGestureFinish = React.useCallback(() => {
onGestureEnd?.();
endInteraction();
- };
- const onGestureAbort = () => {
+ }, [onGestureEnd, endInteraction]);
+ const onGestureAbort = React.useCallback(() => {
onGestureCancel?.();
endInteraction();
- };
+ }, [onGestureCancel, endInteraction]);
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/software-mansion/react-native-gesture-handler/issues/569
- const hitSlop = isRight ?
+ const hitSlop = React.useMemo(() => isRight ?
// Extend hitSlop to the side of the screen when drawer is closed
// This lets the user drag the drawer from the side of the screen
{
@@ -112,7 +113,7 @@ export function Drawer({
} : {
left: 0,
width: isOpen ? undefined : swipeEdgeWidth
- };
+ }, [isRight, isOpen, swipeEdgeWidth]);
const touchStartX = useSharedValue(0);
const touchX = useSharedValue(0);
const translationX = useSharedValue(getDrawerTranslationX(open));
@@ -151,40 +152,43 @@ export function Drawer({
}, [getDrawerTranslationX, handleAnimationEnd, handleAnimationStart, onClose, onOpen, touchStartX, touchX, translationX]);
React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
const startX = useSharedValue(0);
- let pan = Gesture?.Pan().onBegin(event => {
- 'worklet';
+ const pan = React.useMemo(() => {
+ let panGesture = Gesture?.Pan().onBegin(event => {
+ 'worklet';
- startX.value = translationX.value;
- gestureState.value = event.state;
- touchStartX.value = event.x;
- }).onStart(() => {
- 'worklet';
+ startX.value = translationX.value;
+ gestureState.value = event.state;
+ touchStartX.value = event.x;
+ }).onStart(() => {
+ 'worklet';
- runOnJS(onGestureBegin)();
- }).onChange(event => {
- 'worklet';
+ runOnJS(onGestureBegin)();
+ }).onChange(event => {
+ 'worklet';
- touchX.value = event.x;
- translationX.value = startX.value + event.translationX;
- gestureState.value = event.state;
- }).onEnd((event, success) => {
- 'worklet';
+ touchX.value = event.x;
+ translationX.value = startX.value + event.translationX;
+ gestureState.value = event.state;
+ }).onEnd((event, success) => {
+ 'worklet';
- gestureState.value = event.state;
- if (!success) {
- runOnJS(onGestureAbort)();
+ gestureState.value = event.state;
+ if (!success) {
+ runOnJS(onGestureAbort)();
+ }
+ const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
+ // If swiped to right, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
+ // If swiped to left, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
+ toggleDrawer(nextOpen, event.velocityX);
+ runOnJS(onGestureFinish)();
+ }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
+ if (panGesture && configureGestureHandler) {
+ panGesture = configureGestureHandler(panGesture);
}
- const nextOpen = Math.abs(event.translationX) > SWIPE_MIN_OFFSET && Math.abs(event.translationX) > swipeMinVelocity || Math.abs(event.translationX) > swipeMinDistance ? drawerPosition === 'left' ?
- // If swiped to right, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0 :
- // If swiped to left, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0 : open;
- toggleDrawer(nextOpen, event.velocityX);
- runOnJS(onGestureFinish)();
- }).activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET]).hitSlop(hitSlop).enabled(drawerType !== 'permanent' && swipeEnabled);
- if (pan && configureGestureHandler) {
- pan = configureGestureHandler(pan);
- }
+ return panGesture;
+ }, [configureGestureHandler, drawerPosition, drawerType, gestureState, hitSlop, onGestureBegin, onGestureAbort, onGestureFinish, open, startX, swipeEnabled, swipeMinDistance, swipeMinVelocity, toggleDrawer, touchStartX, touchX, translationX]);
const translateX = useDerivedValue(() => {
// Comment stolen from react-native-gesture-handler/DrawerLayout
//
@@ -247,35 +251,38 @@ export function Drawer({
style: [styles.container, style],
children: /*#__PURE__*/_jsx(DrawerProgressContext.Provider, {
value: progress,
- children: /*#__PURE__*/_jsx(GestureDetector, {
- gesture: pan,
- children: /*#__PURE__*/_jsxs(Animated.View, {
- style: [styles.main, {
- flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
- }],
- children: [/*#__PURE__*/_jsxs(Animated.View, {
- style: [styles.content, contentAnimatedStyle],
- children: [/*#__PURE__*/_jsx(View, {
- accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
- importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
- style: styles.content,
- children: children
- }), drawerType !== 'permanent' ? /*#__PURE__*/_jsx(Overlay, {
- open: open,
- progress: progress,
- onPress: () => toggleDrawer(false),
- style: overlayStyle,
- accessibilityLabel: overlayAccessibilityLabel
- }) : null]
- }), /*#__PURE__*/_jsx(Animated.View, {
- removeClippedSubviews: Platform.OS !== 'ios',
- style: [styles.drawer, {
- width: drawerWidth,
- position: drawerType === 'permanent' ? 'relative' : 'absolute',
- zIndex: drawerType === 'back' ? -1 : 0
- }, drawerAnimatedStyle, drawerStyle],
- children: renderDrawerContent()
- })]
+ children: /*#__PURE__*/_jsx(DrawerGestureContext.Provider, {
+ value: pan,
+ children: /*#__PURE__*/_jsx(GestureDetector, {
+ gesture: pan,
+ children: /*#__PURE__*/_jsxs(Animated.View, {
+ style: [styles.main, {
+ flexDirection: drawerType === 'permanent' ? isRight && direction === 'ltr' || !isRight && direction === 'rtl' ? 'row' : 'row-reverse' : 'row'
+ }],
+ children: [/*#__PURE__*/_jsxs(Animated.View, {
+ style: [styles.content, contentAnimatedStyle],
+ children: [/*#__PURE__*/_jsx(View, {
+ accessibilityElementsHidden: isOpen && drawerType !== 'permanent',
+ importantForAccessibility: isOpen && drawerType !== 'permanent' ? 'no-hide-descendants' : 'auto',
+ style: styles.content,
+ children: children
+ }), drawerType !== 'permanent' ? /*#__PURE__*/_jsx(Overlay, {
+ open: open,
+ progress: progress,
+ onPress: () => toggleDrawer(false),
+ style: overlayStyle,
+ accessibilityLabel: overlayAccessibilityLabel
+ }) : null]
+ }), /*#__PURE__*/_jsx(Animated.View, {
+ removeClippedSubviews: Platform.OS !== 'ios',
+ style: [styles.drawer, {
+ width: drawerWidth,
+ position: drawerType === 'permanent' ? 'relative' : 'absolute',
+ zIndex: drawerType === 'back' ? -1 : 0
+ }, drawerAnimatedStyle, drawerStyle],
+ children: renderDrawerContent()
+ })]
+ })
})
})
})
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts
index 7e978f0..a8bce18 100644
--- a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts
+++ b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/index.d.ts
@@ -1,3 +1,4 @@
+export { DrawerGestureContext } from './utils/DrawerGestureContext';
export { DrawerProgressContext } from './utils/DrawerProgressContext';
export { useDrawerProgress } from './utils/useDrawerProgress';
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts
new file mode 100644
index 0000000..33ffbeb
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/typescript/commonjs/src/utils/DrawerGestureContext.d.ts
@@ -0,0 +1,3 @@
+import * as React from 'react';
+export declare const DrawerGestureContext: React.Context<import("react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture").PanGesture | undefined>;
+//# sourceMappingURL=DrawerGestureContext.d.ts.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts
index 7e978f0..a8bce18 100644
--- a/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts
+++ b/node_modules/react-native-drawer-layout/lib/typescript/module/src/index.d.ts
@@ -1,3 +1,4 @@
+export { DrawerGestureContext } from './utils/DrawerGestureContext';
export { DrawerProgressContext } from './utils/DrawerProgressContext';
export { useDrawerProgress } from './utils/useDrawerProgress';
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts b/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts
new file mode 100644
index 0000000..33ffbeb
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/lib/typescript/module/src/utils/DrawerGestureContext.d.ts
@@ -0,0 +1,3 @@
+import * as React from 'react';
+export declare const DrawerGestureContext: React.Context<import("react-native-gesture-handler/lib/typescript/handlers/gestures/panGesture").PanGesture | undefined>;
+//# sourceMappingURL=DrawerGestureContext.d.ts.map
\ No newline at end of file
diff --git a/node_modules/react-native-drawer-layout/src/index.tsx b/node_modules/react-native-drawer-layout/src/index.tsx
index 0aa6f53..61a37cf 100644
--- a/node_modules/react-native-drawer-layout/src/index.tsx
+++ b/node_modules/react-native-drawer-layout/src/index.tsx
@@ -1,3 +1,4 @@
+export { DrawerGestureContext } from './utils/DrawerGestureContext';
export { DrawerProgressContext } from './utils/DrawerProgressContext';
export { useDrawerProgress } from './utils/useDrawerProgress';
export { Drawer } from './views/Drawer';
diff --git a/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx b/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx
new file mode 100644
index 0000000..3aac957
--- /dev/null
+++ b/node_modules/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx
@@ -0,0 +1,6 @@
+import * as React from 'react';
+import type { PanGesture } from 'react-native-gesture-handler';
+
+export const DrawerGestureContext = React.createContext<PanGesture | undefined>(
+ undefined
+);
diff --git a/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx b/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx
index 9c40f41..ee5d075 100644
--- a/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx
+++ b/node_modules/react-native-drawer-layout/src/views/Drawer.native.tsx
@@ -21,6 +21,7 @@ import Animated, {
import useLatestCallback from 'use-latest-callback';
import type { DrawerProps } from '../types';
+import { DrawerGestureContext } from '../utils/DrawerGestureContext';
import { DrawerProgressContext } from '../utils/DrawerProgressContext';
import { getDrawerWidth } from '../utils/getDrawerWidth';
import {
@@ -110,47 +111,51 @@ export function Drawer({
const interactionHandleRef = React.useRef<number | null>(null);
- const startInteraction = () => {
+ const startInteraction = React.useCallback(() => {
interactionHandleRef.current = InteractionManager.createInteractionHandle();
- };
+ }, []);
- const endInteraction = () => {
+ const endInteraction = React.useCallback(() => {
if (interactionHandleRef.current != null) {
InteractionManager.clearInteractionHandle(interactionHandleRef.current);
interactionHandleRef.current = null;
}
- };
+ }, []);
- const hideKeyboard = () => {
+ const hideKeyboard = React.useCallback(() => {
if (keyboardDismissMode === 'on-drag') {
Keyboard.dismiss();
}
- };
+ }, [keyboardDismissMode]);
- const onGestureBegin = () => {
+ const onGestureBegin = React.useCallback(() => {
onGestureStart?.();
startInteraction();
hideKeyboard();
hideStatusBar(true);
- };
+ }, [onGestureStart, startInteraction, hideKeyboard, hideStatusBar]);
- const onGestureFinish = () => {
+ const onGestureFinish = React.useCallback(() => {
onGestureEnd?.();
endInteraction();
- };
+ }, [onGestureEnd, endInteraction]);
- const onGestureAbort = () => {
+ const onGestureAbort = React.useCallback(() => {
onGestureCancel?.();
endInteraction();
- };
+ }, [onGestureCancel, endInteraction]);
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/software-mansion/react-native-gesture-handler/issues/569
- const hitSlop = isRight
- ? // Extend hitSlop to the side of the screen when drawer is closed
- // This lets the user drag the drawer from the side of the screen
- { right: 0, width: isOpen ? undefined : swipeEdgeWidth }
- : { left: 0, width: isOpen ? undefined : swipeEdgeWidth };
+ const hitSlop = React.useMemo(
+ () =>
+ isRight
+ ? // Extend hitSlop to the side of the screen when drawer is closed
+ // This lets the user drag the drawer from the side of the screen
+ { right: 0, width: isOpen ? undefined : swipeEdgeWidth }
+ : { left: 0, width: isOpen ? undefined : swipeEdgeWidth },
+ [isRight, isOpen, swipeEdgeWidth]
+ );
const touchStartX = useSharedValue(0);
const touchX = useSharedValue(0);
@@ -217,53 +222,76 @@ export function Drawer({
const startX = useSharedValue(0);
- let pan = Gesture?.Pan()
- .onBegin((event) => {
- 'worklet';
- startX.value = translationX.value;
- gestureState.value = event.state;
- touchStartX.value = event.x;
- })
- .onStart(() => {
- 'worklet';
- runOnJS(onGestureBegin)();
- })
- .onChange((event) => {
- 'worklet';
- touchX.value = event.x;
- translationX.value = startX.value + event.translationX;
- gestureState.value = event.state;
- })
- .onEnd((event, success) => {
- 'worklet';
- gestureState.value = event.state;
-
- if (!success) {
- runOnJS(onGestureAbort)();
- }
-
- const nextOpen =
- (Math.abs(event.translationX) > SWIPE_MIN_OFFSET &&
- Math.abs(event.translationX) > swipeMinVelocity) ||
- Math.abs(event.translationX) > swipeMinDistance
- ? drawerPosition === 'left'
- ? // If swiped to right, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) > 0
- : // If swiped to left, open the drawer, otherwise close it
- (event.velocityX === 0 ? event.translationX : event.velocityX) < 0
- : open;
-
- toggleDrawer(nextOpen, event.velocityX);
- runOnJS(onGestureFinish)();
- })
- .activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
- .failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
- .hitSlop(hitSlop)
- .enabled(drawerType !== 'permanent' && swipeEnabled);
-
- if (pan && configureGestureHandler) {
- pan = configureGestureHandler(pan);
- }
+ const pan = React.useMemo(() => {
+ let panGesture = Gesture?.Pan()
+ .onBegin((event) => {
+ 'worklet';
+ startX.value = translationX.value;
+ gestureState.value = event.state;
+ touchStartX.value = event.x;
+ })
+ .onStart(() => {
+ 'worklet';
+ runOnJS(onGestureBegin)();
+ })
+ .onChange((event) => {
+ 'worklet';
+ touchX.value = event.x;
+ translationX.value = startX.value + event.translationX;
+ gestureState.value = event.state;
+ })
+ .onEnd((event, success) => {
+ 'worklet';
+ gestureState.value = event.state;
+
+ if (!success) {
+ runOnJS(onGestureAbort)();
+ }
+
+ const nextOpen =
+ (Math.abs(event.translationX) > SWIPE_MIN_OFFSET &&
+ Math.abs(event.translationX) > swipeMinVelocity) ||
+ Math.abs(event.translationX) > swipeMinDistance
+ ? drawerPosition === 'left'
+ ? // If swiped to right, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) >
+ 0
+ : // If swiped to left, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) <
+ 0
+ : open;
+
+ toggleDrawer(nextOpen, event.velocityX);
+ runOnJS(onGestureFinish)();
+ })
+ .activeOffsetX([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
+ .failOffsetY([-SWIPE_MIN_OFFSET, SWIPE_MIN_OFFSET])
+ .hitSlop(hitSlop)
+ .enabled(drawerType !== 'permanent' && swipeEnabled);
+
+ if (panGesture && configureGestureHandler) {
+ panGesture = configureGestureHandler(panGesture);
+ }
+ return panGesture;
+ }, [
+ configureGestureHandler,
+ drawerPosition,
+ drawerType,
+ gestureState,
+ hitSlop,
+ onGestureBegin,
+ onGestureAbort,
+ onGestureFinish,
+ open,
+ startX,
+ swipeEnabled,
+ swipeMinDistance,
+ swipeMinVelocity,
+ toggleDrawer,
+ touchStartX,
+ touchX,
+ translationX,
+ ]);
const translateX = useDerivedValue(() => {
// Comment stolen from react-native-gesture-handler/DrawerLayout
@@ -376,64 +404,66 @@ export function Drawer({
return (
<GestureHandlerRootView style={[styles.container, style]}>
<DrawerProgressContext.Provider value={progress}>
- <GestureDetector gesture={pan}>
- {/* Immediate child of gesture handler needs to be an Animated.View */}
- <Animated.View
- style={[
- styles.main,
- {
- flexDirection:
- drawerType === 'permanent'
- ? (isRight && direction === 'ltr') ||
- (!isRight && direction === 'rtl')
- ? 'row'
- : 'row-reverse'
- : 'row',
- },
- ]}
- >
- <Animated.View style={[styles.content, contentAnimatedStyle]}>
- <View
- accessibilityElementsHidden={
- isOpen && drawerType !== 'permanent'
- }
- importantForAccessibility={
- isOpen && drawerType !== 'permanent'
- ? 'no-hide-descendants'
- : 'auto'
- }
- style={styles.content}
- >
- {children}
- </View>
- {drawerType !== 'permanent' ? (
- <Overlay
- open={open}
- progress={progress}
- onPress={() => toggleDrawer(false)}
- style={overlayStyle}
- accessibilityLabel={overlayAccessibilityLabel}
- />
- ) : null}
- </Animated.View>
+ <DrawerGestureContext.Provider value={pan}>
+ <GestureDetector gesture={pan}>
+ {/* Immediate child of gesture handler needs to be an Animated.View */}
<Animated.View
- removeClippedSubviews={Platform.OS !== 'ios'}
style={[
- styles.drawer,
+ styles.main,
{
- width: drawerWidth,
- position:
- drawerType === 'permanent' ? 'relative' : 'absolute',
- zIndex: drawerType === 'back' ? -1 : 0,
+ flexDirection:
+ drawerType === 'permanent'
+ ? (isRight && direction === 'ltr') ||
+ (!isRight && direction === 'rtl')
+ ? 'row'
+ : 'row-reverse'
+ : 'row',
},
- drawerAnimatedStyle,
- drawerStyle,
]}
>
- {renderDrawerContent()}
+ <Animated.View style={[styles.content, contentAnimatedStyle]}>
+ <View
+ accessibilityElementsHidden={
+ isOpen && drawerType !== 'permanent'
+ }
+ importantForAccessibility={
+ isOpen && drawerType !== 'permanent'
+ ? 'no-hide-descendants'
+ : 'auto'
+ }
+ style={styles.content}
+ >
+ {children}
+ </View>
+ {drawerType !== 'permanent' ? (
+ <Overlay
+ open={open}
+ progress={progress}
+ onPress={() => toggleDrawer(false)}
+ style={overlayStyle}
+ accessibilityLabel={overlayAccessibilityLabel}
+ />
+ ) : null}
+ </Animated.View>
+ <Animated.View
+ removeClippedSubviews={Platform.OS !== 'ios'}
+ style={[
+ styles.drawer,
+ {
+ width: drawerWidth,
+ position:
+ drawerType === 'permanent' ? 'relative' : 'absolute',
+ zIndex: drawerType === 'back' ? -1 : 0,
+ },
+ drawerAnimatedStyle,
+ drawerStyle,
+ ]}
+ >
+ {renderDrawerContent()}
+ </Animated.View>
</Animated.View>
- </Animated.View>
- </GestureDetector>
+ </GestureDetector>
+ </DrawerGestureContext.Provider>
</DrawerProgressContext.Provider>
</GestureHandlerRootView>
);
@@ -1,36 +0,0 @@
diff --git a/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm b/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
index 43d11b4..5bf0d32 100644
--- a/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
+++ b/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
@@ -461,16 +461,23 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
- if ([_handlersToWaitFor count]) {
- RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer];
- if (handler != nil) {
- for (NSNumber *handlerTag in _handlersToWaitFor) {
- if ([handler.tag isEqual:handlerTag]) {
- return YES;
- }
- }
+ RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer];
+ if (handler == nil) {
+ return NO;
+ }
+
+ for (NSNumber *handlerTag in _handlersToWaitFor) {
+ if ([handler.tag isEqual:handlerTag]) {
+ return YES;
}
}
+
+ for (NSNumber *handlerTag in handler->_handlersThatShouldWait) {
+ if ([_tag isEqual:handlerTag]) {
+ return YES;
+ }
+ }
+
return NO;
}
@@ -1,47 +0,0 @@
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml b/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml
index 391f303..8e2c3db 100644
--- a/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml
+++ b/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml
@@ -24,7 +24,7 @@
<activity
android:name="com.yalantis.ucrop.UCropActivity"
- android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
+ android:theme="@style/Theme.UCropNoEdgeToEdge" />
</application>
</manifest>
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/res/values-v35/styles.xml b/node_modules/react-native-image-crop-picker/android/src/main/res/values-v35/styles.xml
new file mode 100644
index 0000000..396d5a8
--- /dev/null
+++ b/node_modules/react-native-image-crop-picker/android/src/main/res/values-v35/styles.xml
@@ -0,0 +1,5 @@
+<resources>
+ <style name="Theme.UCropNoEdgeToEdge" parent="Theme.AppCompat.Light.NoActionBar">
+ <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
+ </style>
+</resources>
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/res/values/styles.xml b/node_modules/react-native-image-crop-picker/android/src/main/res/values/styles.xml
new file mode 100644
index 0000000..50129b6
--- /dev/null
+++ b/node_modules/react-native-image-crop-picker/android/src/main/res/values/styles.xml
@@ -0,0 +1,3 @@
+<resources>
+ <style name="Theme.UCropNoEdgeToEdge" parent="Theme.AppCompat.Light.NoActionBar"/>
+</resources>
diff --git a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
index 9f20973..68d4766 100644
--- a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
+++ b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
@@ -126,7 +126,8 @@ - (void) setConfiguration:(NSDictionary *)options
- (UIViewController*) getRootVC {
UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
- while (root.presentedViewController != nil) {
+ while (root.presentedViewController != nil &&
+ !root.presentedViewController.isBeingDismissed) {
root = root.presentedViewController;
}
-20
View File
@@ -1,20 +0,0 @@
const {withProjectBuildGradle} = require('@expo/config-plugins')
const jitpackRepository = "maven { url 'https://www.jitpack.io' }"
module.exports = function withAndroidNoJitpackPlugin(config) {
return withProjectBuildGradle(config, config => {
if (!config.modResults.contents.includes(jitpackRepository)) {
throw Error(
'Expected to find the jitpack string in the config. ' +
'You MUST verify whether it was actually removed upstream, ' +
'or if the format has changed and this plugin no longer recognizes it.',
)
}
config.modResults.contents = config.modResults.contents.replaceAll(
jitpackRepository,
'',
)
return config
})
}
-34
View File
@@ -1,34 +0,0 @@
const {withGradleProperties} = require('expo/config-plugins')
function setGradlePropertiesValue(config, key, value) {
return withGradleProperties(config, exportedConfig => {
const keyIdx = exportedConfig.modResults.findIndex(
item => item.type === 'property' && item.key === key,
)
if (keyIdx >= 0) {
exportedConfig.modResults.splice(keyIdx, 1, {
type: 'property',
key,
value,
})
} else {
exportedConfig.modResults.push({
type: 'property',
key,
value,
})
}
return exportedConfig
})
}
module.exports = function withGradleJVMHeapSizeIncrease(config) {
config = setGradlePropertiesValue(
config,
'org.gradle.jvmargs',
'-Xmx4096m -XX:MaxMetaspaceSize=1024m', //Set data of your choice
)
return config
}
-70
View File
@@ -1,70 +0,0 @@
const {withAppBuildGradle} = require('@expo/config-plugins')
/**
* A Config Plugin to disable bundle compression in Android build.gradle.
* @param {import('@expo/config-plugins').ConfigPlugin} config
* @returns {import('@expo/config-plugins').ConfigPlugin}
*/
module.exports = function withNoBundleCompression(config) {
return withAppBuildGradle(config, androidConfig => {
let buildGradle = androidConfig.modResults.contents
const hasAndroidResources = buildGradle.includes('androidResources {')
const hasNoCompress = buildGradle.includes('noCompress')
if (hasAndroidResources) {
if (hasNoCompress) {
if (
buildGradle.includes('noCompress += ["bundle"]') ||
buildGradle.includes("noCompress += 'bundle'") ||
buildGradle.includes('noCompress += "bundle"')
) {
return androidConfig
}
const lines = buildGradle.split('\n')
const modifiedLines = lines.map(line => {
if (line.trim().startsWith('noCompress')) {
if (line.includes('+=')) {
return line.replace(/\]/, ', "bundle"]')
} else if (line.includes('=')) {
return line.replace('=', '+= ["bundle",') + ']'
}
}
return line
})
androidConfig.modResults.contents = modifiedLines.join('\n')
} else {
const androidResources = buildGradle.indexOf('androidResources {')
if (androidResources === -1) {
throw new Error(
`Cannot find androidResources { block in build.gradle!`,
)
}
const insertPosition = buildGradle.indexOf('\n', androidResources) + 1
const newContent =
buildGradle.slice(0, insertPosition) +
' noCompress += ["bundle"]\n' +
buildGradle.slice(insertPosition)
androidConfig.modResults.contents = newContent
}
} else {
const androidBlock = buildGradle.indexOf('android {')
if (androidBlock === -1) {
throw new Error(`Cannot find android { block in build.gradle!`)
}
const insertPosition = buildGradle.indexOf('\n', androidBlock) + 1
const newContent =
buildGradle.slice(0, insertPosition) +
' androidResources {\n' +
' noCompress += ["bundle"]\n' +
' }\n' +
buildGradle.slice(insertPosition)
androidConfig.modResults.contents = newContent
}
return androidConfig
})
}
+46 -48
View File
@@ -1,6 +1,6 @@
import 'react-native-url-polyfill/auto'
import '#/logger/sentry/setup'
import '#/logger/bitdrift/setup'
import '#/lib/sentry' // must be near top
import '#/lib/bitdrift' // must be near top
import '#/view/icons'
import React, {useEffect, useState} from 'react'
@@ -10,7 +10,6 @@ import {
initialWindowMetrics,
SafeAreaProvider,
} from 'react-native-safe-area-context'
import * as ScreenOrientation from 'expo-screen-orientation'
import * as SplashScreen from 'expo-splash-screen'
import * as SystemUI from 'expo-system-ui'
import {msg} from '@lingui/macro'
@@ -23,7 +22,7 @@ import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger'
import {isAndroid, isIOS} from '#/platform/detection'
import {isIOS} from '#/platform/detection'
import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
@@ -69,6 +68,7 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PortalProvider} from '#/components/Portal'
import {AppProfiler} from '#/AppProfiler'
import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
@@ -77,10 +77,6 @@ SplashScreen.preventAutoHideAsync()
if (isIOS) {
SystemUI.setBackgroundColorAsync('black')
}
if (isAndroid) {
// iOS is handled by the config plugin -sfn
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP)
}
/**
* Begin geolocation ASAP
@@ -118,7 +114,7 @@ function InnerApp() {
useEffect(() => {
return listenSessionDropped(() => {
Toast.show(
_(msg`Sorry! Your session expired. Please sign in again.`),
_(msg`Sorry! Your session expired. Please log in again.`),
'info',
)
})
@@ -151,11 +147,9 @@ function InnerApp() {
<TrendingConfigProvider>
<GestureHandlerRootView
style={s.h100pct}>
<IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
</IntentDialogProvider>
<TestCtrls />
<Shell />
<NuxDialogs />
</GestureHandlerRootView>
</TrendingConfigProvider>
</ProgressGuideProvider>
@@ -199,40 +193,44 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<GeolocationProvider>
<A11yProvider>
<KeyboardControllerProvider>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<BottomSheetProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<LightStatusBarProvider>
<InnerApp />
</LightStatusBarProvider>
</SafeAreaProvider>
</StarterPackProvider>
</BottomSheetProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardControllerProvider>
</A11yProvider>
</GeolocationProvider>
<AppProfiler>
<GeolocationProvider>
<A11yProvider>
<KeyboardControllerProvider>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<BottomSheetProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<LightStatusBarProvider>
<InnerApp />
</LightStatusBarProvider>
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
</BottomSheetProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardControllerProvider>
</A11yProvider>
</GeolocationProvider>
</AppProfiler>
)
}
+9 -9
View File
@@ -1,4 +1,4 @@
import '#/logger/sentry/setup' // must be near top
import '#/lib/sentry' // must be near top
import '#/view/icons'
import './style.css'
@@ -94,7 +94,7 @@ function InnerApp() {
useEffect(() => {
return listenSessionDropped(() => {
Toast.show(
_(msg`Sorry! Your session expired. Please sign in again.`),
_(msg`Sorry! Your session expired. Please log in again.`),
'info',
)
})
@@ -129,10 +129,8 @@ function InnerApp() {
<SafeAreaProvider>
<ProgressGuideProvider>
<TrendingConfigProvider>
<IntentDialogProvider>
<Shell />
<NuxDialogs />
</IntentDialogProvider>
<Shell />
<NuxDialogs />
</TrendingConfigProvider>
</ProgressGuideProvider>
</SafeAreaProvider>
@@ -189,9 +187,11 @@ function App() {
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<LightStatusBarProvider>
<InnerApp />
</LightStatusBarProvider>
<IntentDialogProvider>
<LightStatusBarProvider>
<InnerApp />
</LightStatusBarProvider>
</IntentDialogProvider>
</StarterPackProvider>
</PortalProvider>
</LightboxStateProvider>
+29
View File
@@ -0,0 +1,29 @@
import React, {Profiler} from 'react'
// Don't let it get stripped out in profiling builds (which apply production Babel preset).
const log = (global as any)['con' + 'sole'].log
function onRender(id: string, phase: string, actualDuration: number) {
if (!__DEV__) {
// This block of code will exist in the production build of the app.
// However, only profiling builds of React call `onRender` so it's dead code in actual production.
const message = `<Profiler> ${id}:${phase} ${
actualDuration > 500
? '(╯°□°)╯ '
: actualDuration > 100
? '[!!] '
: actualDuration > 16
? '[!] '
: ''
}${Math.round(actualDuration)}ms`
log(message)
}
}
export function AppProfiler({children}: {children: React.ReactNode}) {
return (
<Profiler id="app" onRender={onRender}>
{children}
</Profiler>
)
}
+4 -44
View File
@@ -32,7 +32,6 @@ import {
import {RouteParams, State} from '#/lib/routes/types'
import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
import {bskyTitle} from '#/lib/strings/headings'
import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
@@ -69,10 +68,8 @@ import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTest
import HashtagScreen from '#/screens/Hashtag'
import {MessagesScreen} from '#/screens/Messages/ChatList'
import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
import {MessagesInboxScreen} from '#/screens/Messages/Inbox'
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
import {ModerationScreen} from '#/screens/Moderation'
import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
@@ -89,11 +86,9 @@ import {
StarterPackScreenShort,
} from '#/screens/StarterPack/StarterPackScreen'
import {Wizard} from '#/screens/StarterPack/Wizard'
import {VideoFeed} from '#/screens/VideoFeed'
import {useTheme} from '#/alf'
import {router} from '#/routes'
import {Referrer} from '../modules/expo-bluesky-swiss-army'
import {ProfileSearchScreen} from './screens/Profile/ProfileSearch'
import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings'
import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
@@ -105,7 +100,6 @@ import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings'
import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings'
import {SettingsScreen} from './screens/Settings/Settings'
import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences'
import TopicScreen from './screens/Topic'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -159,14 +153,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => ModerationBlockedAccounts}
options={{title: title(msg`Blocked Accounts`), requireAuth: true}}
/>
<Stack.Screen
name="ModerationInteractionSettings"
getComponent={() => ModerationInteractionSettings}
options={{
title: title(msg`Post Interaction Settings`),
requireAuth: true,
}}
/>
<Stack.Screen
name="Settings"
getComponent={() => SettingsScreen}
@@ -210,13 +196,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => ProfileListScreen}
options={{title: title(msg`List`), requireAuth: true}}
/>
<Stack.Screen
name="ProfileSearch"
getComponent={() => ProfileSearchScreen}
options={({route}) => ({
title: title(msg`Search @${route.params.name}'s posts`),
})}
/>
<Stack.Screen
name="PostThread"
getComponent={() => PostThreadScreen}
@@ -397,11 +376,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => HashtagScreen}
options={{title: title(msg`Hashtag`)}}
/>
<Stack.Screen
name="Topic"
getComponent={() => TopicScreen}
options={{title: title(msg`Topic`)}}
/>
<Stack.Screen
name="MessagesConversation"
getComponent={() => MessagesConversationScreen}
@@ -412,11 +386,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Chat settings`), requireAuth: true}}
/>
<Stack.Screen
name="MessagesInbox"
getComponent={() => MessagesInboxScreen}
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
/>
<Stack.Screen
name="NotificationSettings"
getComponent={() => NotificationSettingsScreen}
@@ -447,14 +416,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => Wizard}
options={{title: title(msg`Edit your starter pack`), requireAuth: true}}
/>
<Stack.Screen
name="VideoFeed"
getComponent={() => VideoFeed}
options={{
title: title(msg`Video Feed`),
requireAuth: true,
}}
/>
</>
)
}
@@ -736,16 +697,15 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
linking={LINKING}
theme={theme}
onStateChange={() => {
logger.metric('router:navigate', {
from: prevLoggedRouteName.current,
})
prevLoggedRouteName.current = getCurrentRouteName()
const routeName = getCurrentRouteName()
if (routeName === 'Notifications') {
logEvent('router:navigate:notifications', {})
}
}}
onReady={() => {
attachRouteToLogEvents(getCurrentRouteName)
logModuleInitTime()
onReady()
logger.metric('router:navigate', {})
}}>
{children}
</NavigationContainer>
+3 -63
View File
@@ -31,18 +31,6 @@ export const atoms = {
right: 0,
bottom: 0,
},
top_0: {
top: 0,
},
right_0: {
right: 0,
},
bottom_0: {
bottom: 0,
},
left_0: {
left: 0,
},
z_10: {
zIndex: 10,
},
@@ -62,15 +50,9 @@ export const atoms = {
overflow_hidden: {
overflow: 'hidden',
},
/**
* @platform web
*/
overflow_auto: web({
overflow: 'auto',
}),
/*
* Width & Height
* Width
*/
w_full: {
width: '100%',
@@ -81,17 +63,12 @@ export const atoms = {
h_full_vh: web({
height: '100vh',
}),
max_w_full: {
maxWidth: '100%',
},
max_h_full: {
maxHeight: '100%',
},
/**
* Used for the outermost components on screens, to ensure that they can fill
* the screen and extend beyond.
*/
// @ts-ignore - web only minHeight string
util_screen_outer: [
web({
minHeight: '100vh',
@@ -99,7 +76,7 @@ export const atoms = {
native({
height: '100%',
}),
] as StyleProp<ViewStyle>,
] as ViewStyle,
/*
* Theme-independent bg colors
@@ -111,9 +88,6 @@ export const atoms = {
/*
* Border radius
*/
rounded_0: {
borderRadius: 0,
},
rounded_2xs: {
borderRadius: tokens.borderRadius._2xs,
},
@@ -184,9 +158,6 @@ export const atoms = {
flex_wrap: {
flexWrap: 'wrap',
},
flex_nowrap: {
flexWrap: 'nowrap',
},
flex_0: {
flex: web('0 0 auto') || (native(0) as number),
},
@@ -987,37 +958,6 @@ export const atoms = {
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
transitionDuration: '100ms',
}),
transition_delay_50ms: web({
transitionDelay: '50ms',
}),
/*
* Animaations
*/
fade_in: web({
animation: 'fadeIn ease-out 0.15s',
}),
fade_out: web({
animation: 'fadeOut ease-out 0.15s',
}),
zoom_in: web({
animation: 'zoomIn ease-out 0.1s',
}),
zoom_out: web({
animation: 'zoomOut ease-out 0.1s',
}),
slide_in_left: web({
// exponential easing function
animation: 'slideInLeft cubic-bezier(0.16, 1, 0.3, 1) 0.5s',
}),
slide_out_left: web({
animation: 'slideOutLeft ease-in 0.15s',
animationFillMode: 'forwards',
}),
// special composite animation for dialogs
zoom_fade_in: web({
animation: 'zoomIn ease-out 0.1s, fadeIn ease-out 0.1s',
}),
/**
* {@link Layout.SCROLLBAR_OFFSET}
-15
View File
@@ -26,18 +26,3 @@ export function useBreakpoints(): Record<Breakpoint, boolean> & {
}
}, [gtPhone, gtMobile, gtTablet])
}
/**
* Fine-tuned breakpoints for the shell layout
*/
export function useLayoutBreakpoints() {
const rightNavVisible = useMediaQuery({minWidth: 1075})
const centerColumnOffset = useMediaQuery({minWidth: 1075, maxWidth: 1300})
const leftNavMinimal = useMediaQuery({maxWidth: 1300})
return {
rightNavVisible,
centerColumnOffset,
leftNavMinimal,
}
}
+1 -1
View File
@@ -497,7 +497,7 @@ export function createThemes({
color: dimPalette.contrast_400,
},
text_contrast_medium: {
color: dimPalette.contrast_600,
color: dimPalette.contrast_700,
},
text_contrast_high: {
color: dimPalette.contrast_900,
-3
View File
@@ -1,5 +1,4 @@
import * as NavigationBar from 'expo-navigation-bar'
import * as SystemUI from 'expo-system-ui'
import {isAndroid} from '#/platform/detection'
import {Theme} from '../types'
@@ -10,12 +9,10 @@ export function setNavigationBar(themeType: 'theme' | 'lightbox', t: Theme) {
NavigationBar.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
NavigationBar.setBorderColorAsync(t.atoms.bg.backgroundColor)
NavigationBar.setButtonStyleAsync(t.name !== 'light' ? 'light' : 'dark')
SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
} else {
NavigationBar.setBackgroundColorAsync('black')
NavigationBar.setBorderColorAsync('black')
NavigationBar.setButtonStyleAsync('light')
SystemUI.setBackgroundColorAsync('black')
}
}
}
+2 -6
View File
@@ -62,7 +62,7 @@ export function AccountList({
testID="chooseAddAccountBtn"
style={[a.flex_1]}
onPress={pendingDid ? undefined : onPressAddAccount}
label={_(msg`Sign in to account that is not listed`)}>
label={_(msg`Login to account that is not listed`)}>
{({hovered, pressed}) => (
<View
style={[
@@ -131,11 +131,7 @@ function AccountItem({
(hovered || pressed || isPendingAccount) && t.atoms.bg_contrast_25,
]}>
<View style={a.p_md}>
<UserAvatar
avatar={profile?.avatar}
size={24}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
<UserAvatar avatar={profile?.avatar} size={24} />
</View>
<Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}>
<Text emoji style={[a.font_bold]}>
+10 -18
View File
@@ -2,7 +2,6 @@ import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button as BaseButton, 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'
@@ -50,29 +49,22 @@ export function Text({
return (
<BaseText
{...rest}
style={[a.flex_1, a.text_sm, a.leading_snug, a.pr_md, style]}>
style={[
a.flex_1,
a.text_sm,
a.leading_snug,
{
paddingTop: 1,
},
style,
]}>
{children}
</BaseText>
)
}
export function Button({
children,
...props
}: Omit<ButtonProps, 'size' | 'variant' | 'color'>) {
return (
<BaseButton size="tiny" variant="outline" color="secondary" {...props}>
{children}
</BaseButton>
)
}
export function Row({children}: {children: React.ReactNode}) {
return (
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
{children}
</View>
)
return <View style={[a.flex_row, a.gap_sm]}>{children}</View>
}
export function Outer({
+11 -42
View File
@@ -1,37 +1,37 @@
import {View} from 'react-native'
import {moderateProfile} from '@atproto/api'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfilesQuery} from '#/state/queries/profile'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import * as bsky from '#/types/bsky'
export function AvatarStack({
profiles,
size = 26,
numPending,
backgroundColor,
}: {
profiles: bsky.profile.AnyProfileView[]
profiles: string[]
size?: number
numPending?: number
backgroundColor?: string
}) {
const halfSize = size / 2
const {data, error} = useProfilesQuery({handles: profiles})
const t = useTheme()
const moderationOpts = useModerationOpts()
const isPending = (numPending && profiles.length === 0) || !moderationOpts
if (error) {
console.error(error)
return null
}
const isPending = !data || !moderationOpts
const items = isPending
? Array.from({length: numPending ?? profiles.length}).map((_, i) => ({
? Array.from({length: profiles.length}).map((_, i) => ({
key: i,
profile: null,
moderation: null,
}))
: profiles.map(item => ({
: data.profiles.map(item => ({
key: item.did,
profile: item,
moderation: moderateProfile(item, moderationOpts),
@@ -56,7 +56,7 @@ export function AvatarStack({
height: size,
left: i * -halfSize,
borderWidth: 1,
borderColor: backgroundColor ?? t.atoms.bg.backgroundColor,
borderColor: t.atoms.bg.backgroundColor,
borderRadius: 999,
zIndex: 3 - i,
},
@@ -65,7 +65,6 @@ export function AvatarStack({
<UserAvatar
size={size - 2}
avatar={item.profile.avatar}
type={item.profile.associated?.labeler ? 'labeler' : 'user'}
moderation={item.moderation.ui('avatar')}
/>
)}
@@ -74,33 +73,3 @@ export function AvatarStack({
</View>
)
}
export function AvatarStackWithFetch({
profiles,
size,
backgroundColor,
}: {
profiles: string[]
size?: number
backgroundColor?: string
}) {
const {data, error} = useProfilesQuery({handles: profiles})
if (error) {
if (error.name !== 'AbortError') {
logger.error('Error fetching profiles for AvatarStack', {
safeMessage: error,
})
}
return null
}
return (
<AvatarStack
numPending={profiles.length}
profiles={data?.profiles || []}
size={size}
backgroundColor={backgroundColor}
/>
)
}
+16 -30
View File
@@ -3,12 +3,10 @@ import {
AccessibilityProps,
GestureResponderEvent,
MouseEvent,
NativeSyntheticEvent,
Pressable,
PressableProps,
StyleProp,
StyleSheet,
TargetedEvent,
TextProps,
TextStyle,
View,
@@ -78,8 +76,6 @@ export type ButtonProps = Pick<
| 'onHoverOut'
| 'onPressIn'
| 'onPressOut'
| 'onFocus'
| 'onBlur'
> &
AccessibilityProps &
VariantProps & {
@@ -120,12 +116,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
style,
hoverStyle: hoverStyleProp,
PressableComponent = Pressable,
onPressIn: onPressInOuter,
onPressOut: onPressOutOuter,
onHoverIn: onHoverInOuter,
onHoverOut: onHoverOutOuter,
onFocus: onFocusOuter,
onBlur: onBlurOuter,
...rest
},
ref,
@@ -137,6 +127,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
focused: false,
})
const onPressInOuter = rest.onPressIn
const onPressIn = React.useCallback(
(e: GestureResponderEvent) => {
setState(s => ({
@@ -147,6 +138,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
},
[setState, onPressInOuter],
)
const onPressOutOuter = rest.onPressOut
const onPressOut = React.useCallback(
(e: GestureResponderEvent) => {
setState(s => ({
@@ -157,6 +149,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
},
[setState, onPressOutOuter],
)
const onHoverInOuter = rest.onHoverIn
const onHoverIn = React.useCallback(
(e: MouseEvent) => {
setState(s => ({
@@ -167,6 +160,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
},
[setState, onHoverInOuter],
)
const onHoverOutOuter = rest.onHoverOut
const onHoverOut = React.useCallback(
(e: MouseEvent) => {
setState(s => ({
@@ -177,26 +171,18 @@ export const Button = React.forwardRef<View, ButtonProps>(
},
[setState, onHoverOutOuter],
)
const onFocus = React.useCallback(
(e: NativeSyntheticEvent<TargetedEvent>) => {
setState(s => ({
...s,
focused: true,
}))
onFocusOuter?.(e)
},
[setState, onFocusOuter],
)
const onBlur = React.useCallback(
(e: NativeSyntheticEvent<TargetedEvent>) => {
setState(s => ({
...s,
focused: false,
}))
onBlurOuter?.(e)
},
[setState, onBlurOuter],
)
const onFocus = React.useCallback(() => {
setState(s => ({
...s,
focused: true,
}))
}, [setState])
const onBlur = React.useCallback(() => {
setState(s => ({
...s,
focused: false,
}))
}, [setState])
const {baseStyles, hoverStyles} = React.useMemo(() => {
const baseStyles: ViewStyle[] = []
-1
View File
@@ -14,7 +14,6 @@ export const Context = React.createContext<DialogContextProps>({
nativeSnapPoint: BottomSheetSnapPoint.Hidden,
disableDrag: false,
setDisableDrag: () => {},
isWithinDialog: false,
})
export function useDialogContext() {
+14 -26
View File
@@ -26,8 +26,7 @@ import {isAndroid, isIOS} from '#/platform/detection'
import {useA11y} from '#/state/a11y'
import {useDialogStateControlContext} from '#/state/dialogs'
import {List, ListMethods, ListProps} from '#/view/com/util/List'
import {atoms as a, tokens, useTheme} from '#/alf'
import {useThemeName} from '#/alf/util/useColorModeTheme'
import {atoms as a, useTheme} from '#/alf'
import {Context, useDialogContext} from '#/components/Dialog/context'
import {
DialogControlProps,
@@ -46,7 +45,7 @@ export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/shared'
export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils'
// @ts-ignore
export const Input = createInput(TextInput)
export function Outer({
@@ -56,8 +55,7 @@ export function Outer({
nativeOptions,
testID,
}: React.PropsWithChildren<DialogOuterProps>) {
const themeName = useThemeName()
const t = useTheme(themeName)
const t = useTheme()
const ref = React.useRef<BottomSheetNativeComponent>(null)
const closeCallbacks = React.useRef<(() => void)[]>([])
const {setDialogIsOpen, setFullyExpandedCount} =
@@ -154,7 +152,6 @@ export function Outer({
nativeSnapPoint: snapPoint,
disableDrag,
setDisableDrag,
isWithinDialog: true,
}),
[close, snapPoint, disableDrag, setDisableDrag],
)
@@ -169,9 +166,7 @@ export function Outer({
onStateChange={onStateChange}
disableDrag={disableDrag}>
<Context.Provider value={context}>
<View testID={testID} style={[a.relative]}>
{children}
</View>
<View testID={testID}>{children}</View>
</Context.Provider>
</BottomSheet>
)
@@ -199,7 +194,7 @@ export function Inner({children, style, header}: DialogInnerProps) {
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
function ScrollableInner(
{children, contentContainerStyle, header, ...props},
{children, style, contentContainerStyle, header, ...props},
ref,
) {
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
@@ -219,21 +214,13 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
[],
)
let paddingBottom = 0
if (isIOS) {
paddingBottom += keyboardHeight / 4
if (nativeSnapPoint === BottomSheetSnapPoint.Full) {
paddingBottom += insets.bottom + tokens.space.md
}
paddingBottom = Math.max(paddingBottom, tokens.space._2xl)
} else {
paddingBottom += keyboardHeight
if (nativeSnapPoint === BottomSheetSnapPoint.Full) {
paddingBottom += insets.top
}
paddingBottom +=
Math.max(insets.bottom, tokens.space._5xl) + tokens.space._2xl
}
const basePading =
(isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight)
const fullPaddingBase = insets.bottom + insets.top + basePading
const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50
const paddingBottom =
nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
if (!isAndroid) {
@@ -249,6 +236,7 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
return (
<KeyboardAwareScrollView
style={[style]}
contentContainerStyle={[
a.pt_2xl,
a.px_xl,
@@ -326,7 +314,7 @@ export function Handle() {
style={[
a.rounded_sm,
{
top: tokens.space._2xl / 2 - 2.5,
top: 10,
width: 35,
height: 5,
alignSelf: 'center',
+21 -25
View File
@@ -15,7 +15,6 @@ import {FocusScope} from '@radix-ui/react-focus-scope'
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 {Button, ButtonIcon} from '#/components/Button'
@@ -41,7 +40,6 @@ export function Outer({
children,
control,
onClose,
webOptions,
}: React.PropsWithChildren<DialogOuterProps>) {
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
@@ -97,7 +95,6 @@ export function Outer({
nativeSnapPoint: 0,
disableDrag: false,
setDisableDrag: () => {},
isWithinDialog: true,
}),
[close],
)
@@ -117,26 +114,20 @@ export function Outer({
web(a.fixed),
a.inset_0,
a.z_10,
a.px_xl,
webOptions?.alignCenter ? a.justify_center : undefined,
a.align_center,
{
overflowY: 'auto',
paddingVertical: gtMobile ? '10vh' : a.pt_xl.paddingTop,
},
gtMobile ? a.p_lg : a.p_md,
{overflowY: 'auto'},
]}>
<Backdrop />
{/**
* This is needed to prevent centered dialogs from overflowing
* above the screen, and provides a "natural" centering so that
* stacked dialogs appear relatively aligned.
*/}
<View
style={[
a.w_full,
a.z_20,
a.justify_center,
a.align_center,
web({minHeight: '60vh', position: 'static'}),
{
minHeight: web('calc(90vh - 36px)') || undefined,
},
]}>
{children}
</View>
@@ -161,7 +152,6 @@ export function Inner({
const t = useTheme()
const {close} = React.useContext(Context)
const {gtMobile} = useBreakpoints()
const {reduceMotionEnabled} = useA11y()
useFocusGuards()
return (
<FocusScope loop asChild trapped>
@@ -171,7 +161,7 @@ export function Inner({
aria-label={label}
aria-labelledby={accessibilityLabelledBy}
aria-describedby={accessibilityDescribedBy}
// @ts-expect-error web only -prf
// @ts-ignore web only -prf
onClick={stopPropagation}
onStartShouldSetResponder={_ => true}
onTouchEnd={stopPropagation}
@@ -187,9 +177,10 @@ export function Inner({
shadowColor: t.palette.black,
shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
shadowRadius: 30,
// @ts-ignore web only
animation: 'fadeIn ease-out 0.1s',
},
!reduceMotionEnabled && a.zoom_fade_in,
style,
flatten(style),
])}>
<DismissableLayer
onInteractOutside={preventDefault}
@@ -225,8 +216,8 @@ export const InnerFlatList = React.forwardRef<
style={[
a.overflow_hidden,
a.px_0,
// 100 minus 10vh of paddingVertical
web({maxHeight: '80vh'}),
// @ts-ignore web only -sfn
{maxHeight: 'calc(-36px + 100vh)'},
webInnerStyle,
]}
contentContainerStyle={[a.px_0, webInnerContentContainerStyle]}>
@@ -271,15 +262,20 @@ export function Handle() {
function Backdrop() {
const t = useTheme()
const {reduceMotionEnabled} = useA11y()
return (
<View style={{opacity: 0.8}}>
<View
style={{
opacity: 0.8,
}}>
<View
style={[
a.fixed,
a.inset_0,
{backgroundColor: t.palette.black},
!reduceMotionEnabled && a.fade_in,
{
backgroundColor: t.palette.black,
// @ts-ignore web only
animation: 'fadeIn ease-out 0.15s',
},
]}
/>
</View>
+2 -9
View File
@@ -44,8 +44,6 @@ export type DialogContextProps = {
nativeSnapPoint: BottomSheetSnapPoint
disableDrag: boolean
setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
// in the event that the hook is used outside of a dialog
isWithinDialog: boolean
}
export type DialogControlOpenOptions = {
@@ -62,16 +60,11 @@ export type DialogOuterProps = {
control: DialogControlProps
onClose?: () => void
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
webOptions?: {
alignCenter?: boolean
}
webOptions?: {}
testID?: string
}
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> &
T & {
testID?: string
}
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
export type DialogInnerProps =
| DialogInnerPropsBase<{
label?: undefined
+5 -5
View File
@@ -1,6 +1,7 @@
import React from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyGraphDefs,
AtUri,
@@ -31,7 +32,6 @@ import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {RichText, RichTextProps} from '#/components/RichText'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
type Props = {
view: AppBskyFeedDefs.GeneratorView
@@ -115,7 +115,7 @@ export function TitleAndByline({
creator,
}: {
title: string
creator?: bsky.profile.AnyProfileView
creator?: AppBskyActorDefs.ProfileViewBasic
}) {
const t = useTheme()
@@ -269,9 +269,9 @@ function SaveButtonInner({
},
])
}
Toast.show(_(msg({message: 'Feeds updated!', context: 'toast'})))
Toast.show(_(msg`Feeds updated!`))
} catch (err: any) {
logger.error(err, {message: `FeedCard: failed to update feeds`, pin})
logger.error(err, {context: `FeedCard: failed to update feeds`, pin})
Toast.show(_(msg`Failed to update feeds`), 'xmark')
}
},
@@ -307,7 +307,7 @@ function SaveButtonInner({
<Prompt.Basic
control={removePromptControl}
title={_(msg`Remove from your feeds?`)}
title={_(msg`Remove from my feeds?`)}
description={_(
msg`Are you sure you want to remove this from your feeds?`,
)}
+129
View File
@@ -0,0 +1,129 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
import {isNative} from '#/platform/detection'
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/trending-config'
import {atoms as a, useBreakpoints,useGutters, useTheme, web} from '#/alf'
import {Button, ButtonIcon,ButtonText} from '#/components/Button'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
import {
TrendingTopic,
TrendingTopicLink,
TrendingTopicSkeleton,
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
const TRENDS_COUNT = 9
export function FeedEnd() {
const t = useTheme()
const {_} = useLingui()
const gutters = useGutters(['wide'])
const {enabled} = useTrendingConfig()
const navigation = useNavigationDeduped()
const {gtTablet} = useBreakpoints()
const onPressExplore = React.useCallback(() => {
if (isNative) {
navigation.navigate('SearchTab')
navigation.popToTop()
} else {
navigation.navigate('Search', {})
}
}, [navigation])
return (
<View
style={[
a.align_center,
gutters,
a.py_5xl,
a.border_t,
t.atoms.border_contrast_low,
]}>
<View style={[a.gap_sm, a.pb_2xl]}>
<Text
style={[
a.text_sm,
a.leading_snug,
a.text_center,
{
maxWidth: 200,
},
web({
maxWidth: '12rem',
}),
]}>
<Trans>That's it! You've reached the end of this feed.</Trans>
</Text>
<Button
label={_(msg`Explore Bluesky`)}
onPress={onPressExplore}
size="small"
variant="solid"
color="secondary"
style={[a.justify_center]}>
<ButtonIcon icon={Search} position="left" />
<ButtonText>
<Trans>Explore Bluesky</Trans>
</ButtonText>
</Button>
</View>
{enabled && !gtTablet ? <Trending /> : null}
</View>
)
}
function Trending() {
const t = useTheme()
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
const {gtMobile} = useBreakpoints()
return error || noTopics ? null : (
<View style={[a.gap_sm, gtMobile && a.px_2xl]}>
<View style={[a.flex_row, a.align_center, a.justify_center, a.gap_xs]}>
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>Check out what's trending</Trans>
</Text>
<Graph size="sm" />
</View>
<View style={[a.flex_row, a.flex_wrap, a.justify_center, a.gap_sm]}>
{isLoading ? (
Array(TRENDS_COUNT)
.fill(0)
.map((_n, i) => (
<TrendingTopicSkeleton key={i} size="small" index={i} />
))
) : !trending?.topics ? null : (
<>
{trending.topics.slice(0, TRENDS_COUNT).map(topic => (
<TrendingTopicLink key={topic.link} topic={topic}>
{({hovered}) => (
<TrendingTopic
size="small"
topic={topic}
style={[
hovered && [
t.atoms.border_contrast_high,
t.atoms.bg_contrast_25,
],
]}
/>
)}
</TrendingTopicLink>
))}
</>
)}
</View>
</View>
)
}
+53 -84
View File
@@ -1,7 +1,7 @@
import React from 'react'
import {View} from 'react-native'
import {ScrollView} from 'react-native-gesture-handler'
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
@@ -17,7 +17,6 @@ import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows
import {useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {SeenPost} from '#/state/userActionHistory'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
import {Button} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard'
@@ -27,7 +26,6 @@ import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/P
import {InlineLinkText} from '#/components/Link'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import {ProgressGuideList} from './ProgressGuide/List'
const MOBILE_CARD_WIDTH = 300
@@ -198,7 +196,6 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
<ProfileGrid
isSuggestionsLoading={isSuggestionsLoading}
profiles={data?.suggestions ?? []}
recId={data?.recId}
error={error}
viewContext="profile"
/>
@@ -225,12 +222,10 @@ export function ProfileGrid({
isSuggestionsLoading,
error,
profiles,
recId,
viewContext = 'feed',
}: {
isSuggestionsLoading: boolean
profiles: bsky.profile.AnyProfileView[]
recId?: number
profiles: AppBskyActorDefs.ProfileViewDetailed[]
error: Error | null
viewContext: 'profile' | 'feed'
}) {
@@ -254,19 +249,12 @@ export function ProfileGrid({
))
) : error || !profiles.length ? null : (
<>
{profiles.slice(0, maxLength).map((profile, index) => (
{profiles.slice(0, maxLength).map(profile => (
<ProfileCard.Link
key={profile.did}
profile={profile}
onPress={() => {
logEvent('suggestedUser:press', {
logContext:
viewContext === 'feed'
? 'InterstitialDiscover'
: 'InterstitialProfile',
recId,
position: index,
})
logEvent('feed:interstitial:profileCard:press', {})
}}
style={[
a.flex_1,
@@ -294,17 +282,6 @@ export function ProfileGrid({
logContext="FeedInterstitial"
shape="round"
colorInverted
onFollow={() => {
logEvent('suggestedUser:follow', {
logContext:
viewContext === 'feed'
? 'InterstitialDiscover'
: 'InterstitialProfile',
location: 'Card',
recId,
position: index,
})
}}
/>
</ProfileCard.Header>
<ProfileCard.Description profile={profile} numberOfLines={2} />
@@ -359,37 +336,33 @@ export function ProfileGrid({
</View>
</View>
) : (
<BlockDrawerGesture>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
decelerationRate="fast">
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
{content}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
decelerationRate="fast">
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
{content}
<Button
label={_(msg`Browse more accounts on the Explore page`)}
onPress={() => {
navigation.navigate('SearchTab')
}}>
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
<View style={[a.flex_1, a.justify_center]}>
<View style={[a.flex_row, a.px_lg]}>
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
<Trans>
Browse more suggestions on the Explore page
</Trans>
</Text>
<Button
label={_(msg`Browse more accounts on the Explore page`)}
onPress={() => {
navigation.navigate('SearchTab')
}}>
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
<View style={[a.flex_1, a.justify_center]}>
<View style={[a.flex_row, a.px_lg]}>
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
<Trans>Browse more suggestions on the Explore page</Trans>
</Text>
<Arrow size="xl" />
</View>
<Arrow size="xl" />
</View>
</CardOuter>
</Button>
</View>
</ScrollView>
</BlockDrawerGesture>
</View>
</CardOuter>
</Button>
</View>
</ScrollView>
)}
</View>
)
@@ -496,38 +469,34 @@ export function SuggestedFeeds() {
</View>
</View>
) : (
<BlockDrawerGesture>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
decelerationRate="fast">
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
{content}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
decelerationRate="fast">
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
{content}
<Button
label={_(msg`Browse more feeds on the Explore page`)}
onPress={() => {
navigation.navigate('SearchTab')
}}
style={[a.flex_col]}>
<CardOuter style={[a.flex_1]}>
<View style={[a.flex_1, a.justify_center]}>
<View style={[a.flex_row, a.px_lg]}>
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
<Trans>
Browse more suggestions on the Explore page
</Trans>
</Text>
<Button
label={_(msg`Browse more feeds on the Explore page`)}
onPress={() => {
navigation.navigate('SearchTab')
}}
style={[a.flex_col]}>
<CardOuter style={[a.flex_1]}>
<View style={[a.flex_1, a.justify_center]}>
<View style={[a.flex_row, a.px_lg]}>
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
<Trans>Browse more suggestions on the Explore page</Trans>
</Text>
<Arrow size="xl" />
</View>
<Arrow size="xl" />
</View>
</CardOuter>
</Button>
</View>
</ScrollView>
</BlockDrawerGesture>
</View>
</CardOuter>
</Button>
</View>
</ScrollView>
)}
</View>
)
-59
View File
@@ -1,59 +0,0 @@
import {createContext, useContext, useMemo} from 'react'
import {View} from 'react-native'
import {atoms as a, ViewStyleProp} from '#/alf'
const Context = createContext({
gap: 0,
})
export function Row({
children,
gap = 0,
style,
}: ViewStyleProp & {
children: React.ReactNode
gap?: number
}) {
return (
<Context.Provider value={useMemo(() => ({gap}), [gap])}>
<View
style={[
a.flex_row,
a.flex_1,
{
marginLeft: -gap / 2,
marginRight: -gap / 2,
},
style,
]}>
{children}
</View>
</Context.Provider>
)
}
export function Col({
children,
width = 1,
style,
}: ViewStyleProp & {
children: React.ReactNode
width?: number
}) {
const {gap} = useContext(Context)
return (
<View
style={[
a.flex_col,
{
paddingLeft: gap / 2,
paddingRight: gap / 2,
width: `${width * 100}%`,
},
style,
]}>
{children}
</View>
)
}
+11 -26
View File
@@ -10,7 +10,6 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Link, LinkProps} from '#/components/Link'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
const AVI_SIZE = 30
const AVI_SIZE_SMALL = 20
@@ -33,13 +32,11 @@ export function KnownFollowers({
moderationOpts,
onLinkPress,
minimal,
showIfEmpty,
}: {
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
onLinkPress?: LinkProps['onPress']
minimal?: boolean
showIfEmpty?: boolean
}) {
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
new Map(),
@@ -66,12 +63,11 @@ export function KnownFollowers({
moderationOpts={moderationOpts}
onLinkPress={onLinkPress}
minimal={minimal}
showIfEmpty={showIfEmpty}
/>
)
}
return <EmptyFallback show={showIfEmpty} />
return null
}
function KnownFollowersInner({
@@ -80,19 +76,22 @@ function KnownFollowersInner({
cachedKnownFollowers,
onLinkPress,
minimal,
showIfEmpty,
}: {
profile: bsky.profile.AnyProfileView
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
onLinkPress?: LinkProps['onPress']
minimal?: boolean
showIfEmpty?: boolean
}) {
const t = useTheme()
const {_} = useLingui()
const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]
const textStyle = [
a.flex_1,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_medium,
]
const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
const moderation = moderateProfile(f, moderationOpts)
@@ -115,7 +114,7 @@ function KnownFollowersInner({
* We check above too, but here for clarity and a reminder to _check for
* valid indices_
*/
if (slice.length === 0) return <EmptyFallback show={showIfEmpty} />
if (slice.length === 0) return null
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
@@ -127,7 +126,7 @@ function KnownFollowersInner({
onPress={onLinkPress}
to={makeProfileLink(profile, 'known-followers')}
style={[
a.max_w_full,
a.flex_1,
a.flex_row,
minimal ? a.gap_sm : a.gap_md,
a.align_center,
@@ -164,7 +163,6 @@ function KnownFollowersInner({
size={SIZE}
avatar={prof.avatar}
moderation={moderation.ui('avatar')}
type={prof.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
))}
@@ -172,7 +170,6 @@ function KnownFollowersInner({
<Text
style={[
a.flex_shrink,
textStyle,
hovered && {
textDecorationLine: 'underline',
@@ -244,15 +241,3 @@ function KnownFollowersInner({
</Link>
)
}
function EmptyFallback({show}: {show?: boolean}) {
const t = useTheme()
if (!show) return null
return (
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>Not followed by anyone you're following</Trans>
</Text>
)
}
+14 -24
View File
@@ -1,5 +1,5 @@
import {createContext, useCallback, useContext} from 'react'
import {GestureResponderEvent, Keyboard, View} from 'react-native'
import {GestureResponderEvent, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
@@ -14,9 +14,7 @@ import {
TextStyleProp,
useBreakpoints,
useGutters,
useLayoutBreakpoints,
useTheme,
web,
} from '#/alf'
import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
@@ -24,7 +22,6 @@ import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {
BUTTON_VISUAL_ALIGNMENT_OFFSET,
HEADER_SLOT_SIZE,
SCROLLBAR_OFFSET,
} from '#/components/Layout/const'
import {ScrollbarOffsetContext} from '#/components/Layout/context'
import {Text} from '#/components/Typography'
@@ -32,30 +29,23 @@ import {Text} from '#/components/Typography'
export function Outer({
children,
noBottomBorder,
headerRef,
sticky = true,
}: {
children: React.ReactNode
noBottomBorder?: boolean
headerRef?: React.MutableRefObject<View | null>
sticky?: boolean
}) {
const t = useTheme()
const gutters = useGutters([0, 'base'])
const {gtMobile} = useBreakpoints()
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
const {centerColumnOffset} = useLayoutBreakpoints()
return (
<View
ref={headerRef}
style={[
a.w_full,
!noBottomBorder && a.border_b,
a.flex_row,
a.align_center,
a.gap_sm,
sticky && web([a.sticky, {top: 0}, a.z_10, t.atoms.bg]),
gutters,
platform({
native: [a.pb_xs, {minHeight: 48}],
@@ -63,12 +53,7 @@ export function Outer({
}),
t.atoms.border_contrast_low,
gtMobile && [a.mx_auto, {maxWidth: 600}],
!isWithinOffsetView && {
transform: [
{translateX: centerColumnOffset ? -150 : 0},
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
],
},
!isWithinOffsetView && a.scrollbar_offset,
]}>
{children}
</View>
@@ -100,7 +85,17 @@ export function Content({
}
export function Slot({children}: {children?: React.ReactNode}) {
return <View style={[a.z_50, {width: HEADER_SLOT_SIZE}]}>{children}</View>
return (
<View
style={[
a.z_50,
{
width: HEADER_SLOT_SIZE,
},
]}>
{children}
</View>
)
}
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
@@ -130,11 +125,7 @@ export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
shape="square"
onPress={onPressBack}
hitSlop={HITSLOP_30}
style={[
{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET},
a.bg_transparent,
style,
]}
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]}
{...props}>
<ButtonIcon icon={ArrowLeft} size="lg" />
</Button>
@@ -148,7 +139,6 @@ export function MenuButton() {
const {gtMobile} = useBreakpoints()
const onPress = useCallback(() => {
Keyboard.dismiss()
setDrawerOpen(true)
}, [setDrawerOpen])
+8 -37
View File
@@ -13,15 +13,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {isWeb} from '#/platform/detection'
import {useShellLayout} from '#/state/shell/shell-layout'
import {
atoms as a,
useBreakpoints,
useLayoutBreakpoints,
useTheme,
web,
} from '#/alf'
import {useDialogContext} from '#/components/Dialog'
import {SCROLLBAR_OFFSET} from '#/components/Layout/const'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {ScrollbarOffsetContext} from '#/components/Layout/context'
export * from '#/components/Layout/const'
@@ -55,7 +47,6 @@ export const Screen = React.memo(function Screen({
export type ContentProps = AnimatedScrollViewProps & {
style?: StyleProp<ViewStyle>
contentContainerStyle?: StyleProp<ViewStyle>
ignoreTabletLayoutOffset?: boolean
}
/**
@@ -65,10 +56,8 @@ export const Content = React.memo(function Content({
children,
style,
contentContainerStyle,
ignoreTabletLayoutOffset,
...props
}: ContentProps) {
const t = useTheme()
const {footerHeight} = useShellLayout()
const animatedProps = useAnimatedProps(() => {
return {
@@ -84,7 +73,6 @@ export const Content = React.memo(function Content({
<Animated.ScrollView
id="content"
automaticallyAdjustsScrollIndicatorInsets={false}
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
// sets the scroll inset to the height of the footer
animatedProps={animatedProps}
style={[scrollViewStyles.common, style]}
@@ -94,10 +82,8 @@ export const Content = React.memo(function Content({
]}
{...props}>
{isWeb ? (
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
{/* @ts-expect-error web only -esb */}
{children}
</Center>
// @ts-ignore web only -esb
<Center>{children}</Center>
) : (
children
)}
@@ -150,13 +136,10 @@ export const KeyboardAwareContent = React.memo(function LayoutScrollView({
export const Center = React.memo(function LayoutContent({
children,
style,
ignoreTabletLayoutOffset,
...props
}: ViewProps & {ignoreTabletLayoutOffset?: boolean}) {
}: ViewProps) {
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
const {gtMobile} = useBreakpoints()
const {centerColumnOffset} = useLayoutBreakpoints()
const {isWithinDialog} = useDialogContext()
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
return (
<View
@@ -166,20 +149,8 @@ export const Center = React.memo(function LayoutContent({
gtMobile && {
maxWidth: 600,
},
!isWithinOffsetView && {
transform: [
{
translateX:
centerColumnOffset &&
!ignoreTabletLayoutOffset &&
!isWithinDialog
? -150
: 0,
},
{translateX: web(SCROLLBAR_OFFSET) ?? 0},
],
},
style,
!isWithinOffsetView && a.scrollbar_offset,
]}
{...props}>
<ScrollbarOffsetContext.Provider value={ctx}>
@@ -195,7 +166,6 @@ export const Center = React.memo(function LayoutContent({
const WebCenterBorders = React.memo(function LayoutContent() {
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {centerColumnOffset} = useLayoutBreakpoints()
return gtMobile ? (
<View
style={[
@@ -208,8 +178,9 @@ const WebCenterBorders = React.memo(function LayoutContent() {
width: 602,
left: '50%',
transform: [
{translateX: '-50%'},
{translateX: centerColumnOffset ? -150 : 0},
{
translateX: '-50%',
},
...a.scrollbar_offset.transform,
],
}),
+4 -10
View File
@@ -6,18 +6,12 @@ import {gradients} from '#/alf/tokens'
export function LinearGradientBackground({
style,
gradient = 'sky',
children,
start,
end,
}: {
style?: StyleProp<ViewStyle>
gradient?: keyof typeof gradients
children?: React.ReactNode
start?: [number, number]
end?: [number, number]
style: StyleProp<ViewStyle>
children: React.ReactNode
}) {
const colors = gradients[gradient].values.map(([_, color]) => {
const gradient = gradients.sky.values.map(([_, color]) => {
return color
}) as [string, string, ...string[]]
@@ -26,7 +20,7 @@ export function LinearGradientBackground({
}
return (
<LinearGradient colors={colors} style={style} start={start} end={end}>
<LinearGradient colors={gradient} style={style}>
{children}
</LinearGradient>
)
+24 -132
View File
@@ -15,6 +15,7 @@ import {
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
import {isNative, isWeb} from '#/platform/detection'
import {shouldClickOpenNewTab} from '#/platform/urls'
import {useModalControls} from '#/state/modals'
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
@@ -54,12 +55,6 @@ type BaseLinkProps = Pick<
*/
onPress?: (e: GestureResponderEvent) => void | false
/**
* Callback for when the link is long pressed (on native). Prevent default
* and return `false` to exit early and prevent default long press hander.
*/
onLongPress?: (e: GestureResponderEvent) => void | false
/**
* Web-only attribute. Sets `download` attr on web.
*/
@@ -69,11 +64,6 @@ type BaseLinkProps = Pick<
* Native-only attribute. If true, will open the share sheet on long press.
*/
shareOnLongPress?: boolean
/**
* Whether the link should be opened through the redirect proxy.
*/
shouldProxy?: boolean
}
export function useLink({
@@ -82,14 +72,9 @@ export function useLink({
action = 'push',
disableMismatchWarning,
onPress: outerOnPress,
onLongPress: outerOnLongPress,
shareOnLongPress,
overridePresentation,
shouldProxy,
}: BaseLinkProps & {
displayText: string
overridePresentation?: boolean
shouldProxy?: boolean
}) {
const navigation = useNavigationDeduped()
const {href} = useLinkProps<AllNavigatorParams>({
@@ -125,7 +110,7 @@ export function useLink({
})
} else {
if (isExternal) {
openLink(href, overridePresentation, shouldProxy)
openLink(href)
} else {
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
@@ -167,8 +152,6 @@ export function useLink({
closeModal,
action,
navigation,
overridePresentation,
shouldProxy,
],
)
@@ -192,14 +175,8 @@ export function useLink({
}
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
const onLongPress = React.useCallback(
(e: GestureResponderEvent) => {
const exitEarlyIfFalse = outerOnLongPress?.(e)
if (exitEarlyIfFalse === false) return
return isNative && shareOnLongPress ? handleLongPress() : undefined
},
[outerOnLongPress, handleLongPress, shareOnLongPress],
)
const onLongPress =
isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
return {
isExternal,
@@ -225,18 +202,14 @@ export function Link({
to,
action = 'push',
onPress: outerOnPress,
onLongPress: outerOnLongPress,
download,
shouldProxy,
...rest
}: LinkProps) {
const {href, isExternal, onPress, onLongPress} = useLink({
const {href, isExternal, onPress} = useLink({
to,
displayText: typeof children === 'string' ? children : '',
action,
onPress: outerOnPress,
onLongPress: outerOnLongPress,
shouldProxy: shouldProxy,
})
return (
@@ -247,7 +220,6 @@ export function Link({
accessibilityRole="link"
href={href}
onPress={download ? undefined : onPress}
onLongPress={onLongPress}
{...web({
hrefAttrs: {
target: download ? undefined : isExternal ? 'blank' : undefined,
@@ -267,13 +239,12 @@ export function Link({
export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps &
TextStyleProp &
Pick<TextProps, 'selectable' | 'numberOfLines'> &
Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
disableUnderline?: boolean
title?: TextProps['title']
overridePresentation?: boolean
}
>
Pick<TextProps, 'selectable' | 'numberOfLines'>
> &
Pick<ButtonProps, 'label'> & {
disableUnderline?: boolean
title?: TextProps['title']
}
export function InlineLinkText({
children,
@@ -282,14 +253,11 @@ export function InlineLinkText({
disableMismatchWarning,
style,
onPress: outerOnPress,
onLongPress: outerOnLongPress,
download,
selectable,
label,
shareOnLongPress,
disableUnderline,
overridePresentation,
shouldProxy,
...rest
}: InlineLinkProps) {
const t = useTheme()
@@ -300,10 +268,7 @@ export function InlineLinkText({
action,
disableMismatchWarning,
onPress: outerOnPress,
onLongPress: outerOnLongPress,
shareOnLongPress,
overridePresentation,
shouldProxy: shouldProxy,
})
const {
state: hovered,
@@ -354,21 +319,6 @@ export function InlineLinkText({
)
}
export function WebOnlyInlineLinkText({
children,
to,
onPress,
...props
}: Omit<InlineLinkProps, 'onLongPress'>) {
return isWeb ? (
<InlineLinkText {...props} to={to} onPress={onPress}>
{children}
</InlineLinkText>
) : (
<Text {...props}>{children}</Text>
)
}
/**
* Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI
*
@@ -377,10 +327,7 @@ export function WebOnlyInlineLinkText({
*/
export function createStaticClick(
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
): {
to: BaseLinkProps['to']
onPress: Exclude<BaseLinkProps['onPress'], undefined>
} {
): Pick<BaseLinkProps, 'to' | 'onPress'> {
return {
to: '#',
onPress(e: GestureResponderEvent) {
@@ -391,72 +338,17 @@ export function createStaticClick(
}
}
/**
* Utility to create a static `onPress` handler for a `Link`, but only if the
* click was not modified in some way e.g. `Cmd` or a middle click.
*
* On native, this behaves the same as `createStaticClick` because there are no
* options to "modify" the click in this sense.
*
* Example:
* `<Link {...createStaticClick(e => {...})} />`
*/
export function createStaticClickIfUnmodified(
onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>,
): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} {
return {
onPress(e: GestureResponderEvent) {
if (!isWeb || !isModifiedClickEvent(e)) {
e.preventDefault()
onPressHandler(e)
return false
}
},
}
}
/**
* Determines if the click event has a meta key pressed, indicating the user
* intends to deviate from default behavior.
*/
export function isClickEventWithMetaKey(e: GestureResponderEvent) {
if (!isWeb) return false
const event = e as unknown as MouseEvent
return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey
}
/**
* Determines if the web click target is anything other than `_self`
*/
export function isClickTargetExternal(e: GestureResponderEvent) {
if (!isWeb) return false
const event = e as unknown as MouseEvent
const el = event.currentTarget as HTMLAnchorElement
return el && el.target && el.target !== '_self'
}
/**
* Determines if a click event has been modified in some way from its default
* behavior, e.g. `Cmd` or a middle click.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
*/
export function isModifiedClickEvent(e: GestureResponderEvent): boolean {
if (!isWeb) return false
const event = e as unknown as MouseEvent
const isPrimaryButton = event.button === 0
return (
isClickEventWithMetaKey(e) || isClickTargetExternal(e) || !isPrimaryButton
export function WebOnlyInlineLinkText({
children,
to,
onPress,
...props
}: InlineLinkProps) {
return isWeb ? (
<InlineLinkText {...props} to={to} onPress={onPress}>
{children}
</InlineLinkText>
) : (
<Text {...props}>{children}</Text>
)
}
/**
* Determines if a click event has been modified in a way that should indiciate
* that the user intends to open a new tab.
* {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
*/
export function shouldClickOpenNewTab(e: GestureResponderEvent) {
if (!isWeb) return false
const event = e as unknown as MouseEvent
const isMiddleClick = isWeb && event.button === 1
return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick
}
+2 -2
View File
@@ -1,6 +1,7 @@
import React from 'react'
import {View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyGraphDefs,
AtUri,
moderateUserList,
@@ -25,7 +26,6 @@ import {
import {Link as InternalLink, LinkProps} from '#/components/Link'
import * as Hider from '#/components/moderation/Hider'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
/*
* This component is based on `FeedCard` and is tightly coupled with that
@@ -107,7 +107,7 @@ export function TitleAndByline({
modUi,
}: {
title: string
creator?: bsky.profile.AnyProfileView
creator?: AppBskyActorDefs.ProfileViewBasic
purpose?: AppBskyGraphDefs.ListView['purpose']
modUi?: ModerationUI
}) {
+3 -9
View File
@@ -20,7 +20,6 @@ export function ListFooter({
style,
showEndMessage = false,
endMessageText,
renderEndMessage,
}: {
isFetchingNextPage?: boolean
hasNextPage?: boolean
@@ -30,7 +29,6 @@ export function ListFooter({
style?: StyleProp<ViewStyle>
showEndMessage?: boolean
endMessageText?: string
renderEndMessage?: () => React.ReactNode
}) {
const t = useTheme()
@@ -50,13 +48,9 @@ export function ListFooter({
) : error ? (
<ListFooterMaybeError error={error} onRetry={onRetry} />
) : !hasNextPage && showEndMessage ? (
renderEndMessage ? (
renderEndMessage()
) : (
<Text style={[a.text_sm, t.atoms.text_contrast_low]}>
{endMessageText ?? <Trans>You have reached the end</Trans>}
</Text>
)
<Text style={[a.text_sm, t.atoms.text_contrast_low]}>
{endMessageText ?? <Trans>You have reached the end</Trans>}
</Text>
) : null}
</View>
)
+36 -22
View File
@@ -1,15 +1,19 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {Image} from 'expo-image'
import {AppBskyFeedDefs} from '@atproto/api'
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {isTenorGifUri} from '#/lib/strings/embed-player'
import {parseTenorGif} from '#/lib/strings/embed-player'
import {atoms as a, useTheme} from '#/alf'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {Text} from '#/components/Typography'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
import * as bsky from '#/types/bsky'
/**
* Streamlined MediaPreview component which just handles images, gifs, and videos
@@ -18,17 +22,20 @@ export function Embed({
embed,
style,
}: {
embed: AppBskyFeedDefs.PostView['embed']
embed?:
| AppBskyEmbedImages.View
| AppBskyEmbedRecordWithMedia.View
| AppBskyEmbedExternal.View
| AppBskyEmbedVideo.View
| {[k: string]: unknown}
style?: StyleProp<ViewStyle>
}) {
const e = bsky.post.parseEmbed(embed)
let media = AppBskyEmbedRecordWithMedia.isView(embed) ? embed.media : embed
if (!e) return null
if (e.type === 'images') {
if (AppBskyEmbedImages.isView(media)) {
return (
<Outer style={style}>
{e.view.images.map(image => (
{media.images.map(image => (
<ImageItem
key={image.thumb}
thumbnail={image.thumb}
@@ -37,21 +44,28 @@ export function Embed({
))}
</Outer>
)
} else if (e.type === 'link') {
if (!e.view.external.thumb) return null
if (!isTenorGifUri(e.view.external.uri)) return null
} else if (AppBskyEmbedExternal.isView(media) && media.external.thumb) {
let url: URL | undefined
try {
url = new URL(media.external.uri)
} catch {}
if (url) {
const {success} = parseTenorGif(url)
if (success) {
return (
<Outer style={style}>
<GifItem
thumbnail={media.external.thumb}
alt={media.external.title}
/>
</Outer>
)
}
}
} else if (AppBskyEmbedVideo.isView(media)) {
return (
<Outer style={style}>
<GifItem
thumbnail={e.view.external.thumb}
alt={e.view.external.title}
/>
</Outer>
)
} else if (e.type === 'video') {
return (
<Outer style={style}>
<VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} />
<VideoItem thumbnail={media.thumbnail} alt={media.alt} />
</Outer>
)
}
+5 -73
View File
@@ -4,7 +4,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import flattenReactChildren from 'react-keyed-flatten-children'
import {isAndroid, isIOS, isNative} from '#/platform/detection'
import {isNative} from '#/platform/detection'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -47,12 +47,7 @@ export function Root({
return <Context.Provider value={context}>{children}</Context.Provider>
}
export function Trigger({
children,
label,
role = 'button',
hint,
}: TriggerProps) {
export function Trigger({children, label, role = 'button'}: TriggerProps) {
const context = useMenuContext()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
@@ -70,13 +65,11 @@ export function Trigger({
pressed,
},
props: {
ref: null,
onPress: context.control.open,
onFocus,
onBlur,
onPressIn,
onPressOut,
accessibilityHint: hint,
accessibilityLabel: label,
accessibilityRole: role,
},
@@ -100,7 +93,7 @@ export function Outer({
<Dialog.Handle />
{/* Re-wrap with context since Dialogs are portal-ed to root */}
<Context.Provider value={context}>
<Dialog.ScrollableInner label={_(msg`Menu`)}>
<Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.py_sm]}>
<View style={[a.gap_lg]}>
{children}
{isNative && showCancel && <Cancel />}
@@ -129,21 +122,9 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
onFocus={onFocus}
onBlur={onBlur}
onPress={async e => {
if (isAndroid) {
/**
* Below fix for iOS doesn't work for Android, this does.
*/
context.control.close(() => {
onPress?.(e)
context.control.close()
} else if (isIOS) {
/**
* Fixes a subtle bug on iOS
* {@link https://github.com/bluesky-social/social-app/pull/5849/files#diff-de516ef5e7bd9840cd639213301df38cf03acfcad5bda85a1d63efd249ba79deL124-L127}
*/
context.control.close(() => {
onPress?.(e)
})
}
})
}}
onPressIn={e => {
onPressIn()
@@ -209,55 +190,6 @@ export function ItemIcon({icon: Comp}: ItemIconProps) {
)
}
export function ItemRadio({selected}: {selected: boolean}) {
const t = useTheme()
return (
<View
style={[
a.justify_center,
a.align_center,
a.rounded_full,
t.atoms.border_contrast_high,
{
borderWidth: 1,
height: 20,
width: 20,
},
]}>
{selected ? (
<View
style={[
a.absolute,
a.rounded_full,
{height: 14, width: 14},
selected
? {
backgroundColor: t.palette.primary_500,
}
: {},
]}
/>
) : null}
</View>
)
}
export function LabelText({children}: {children: React.ReactNode}) {
const t = useTheme()
return (
<Text
style={[
a.font_bold,
t.atoms.text_contrast_medium,
{
marginBottom: -8,
},
]}>
{children}
</Text>
)
}
export function Group({children, style}: GroupProps) {
const t = useTheme()
return (

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