Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 615bbc3532 | |||
| 59709ad8d8 | |||
| 85ad52e398 | |||
| 3e0ed3b834 | |||
| c11a90124c | |||
| 9f075b1340 | |||
| 09297d92cb | |||
| 2300410b4d | |||
| 30d110dbcd | |||
| af94b79e2c | |||
| 6e94de9f83 | |||
| 9dc04bbfb0 | |||
| eb3bfc9ff0 | |||
| ad42a9297a | |||
| 97288983c3 | |||
| 0118fe75e0 | |||
| e44726529c | |||
| 619da81911 | |||
| cf1ccdf8e3 | |||
| 444c4a944e | |||
| 8af540f284 | |||
| 6c40efb3a3 | |||
| 2dc6932f29 | |||
| c6d26a0a9c | |||
| 8b7a331815 | |||
| d27ed32188 | |||
| 6c9e1d4837 | |||
| f05962a492 | |||
| 2b28c85b43 | |||
| 1e4ea1a271 | |||
| 08a5f7a0bd | |||
| 5020e4233a | |||
| 190c7d6ed8 | |||
| 9d4ae02300 | |||
| fa2072cefa | |||
| 3aa1a5f884 | |||
| 7d86bba7f1 | |||
| f5a9ac1313 | |||
| c5421596a8 | |||
| 8f7a6e5ffc | |||
| aeb78daf84 | |||
| 8116d12c15 | |||
| 8a3dfcb9d0 | |||
| 6afa44ddcb | |||
| ab0697a0ec | |||
| 7f63679a5e | |||
| 975e61d450 | |||
| 10981eec50 | |||
| fb920f89d1 | |||
| 25d20c6395 | |||
| a3031de19b | |||
| 59739a6088 | |||
| 24540400d9 | |||
| 513a188a21 | |||
| 29dad38ab7 | |||
| c390c32788 | |||
| c2ca84c636 | |||
| e2dfe3f438 | |||
| 9ebd362224 | |||
| 25f3aeaa08 | |||
| 6e58a1e8e1 | |||
| 62f43ee15a | |||
| 2e598f7bd7 | |||
| 2aeb098b4e | |||
| 1eb17ac9fa | |||
| 3408d47a1b | |||
| 64dbaed24c | |||
| bdae89b67b | |||
| 0e7ad19830 | |||
| c9aa2b1e30 | |||
| 323c974c6b | |||
| 9b35521d3c | |||
| 45c6308607 | |||
| 65eccc79aa | |||
| 8f171451ef | |||
| ac42f90e4d | |||
| 2e72b0a7a2 | |||
| bc948cb725 | |||
| e7a34669ff | |||
| 4697b92b6a | |||
| d42f768347 | |||
| afd338b0f3 | |||
| 2d1a5e71ab | |||
| 925a240ec4 | |||
| 62daf047ca | |||
| 674153c725 | |||
| c8e307b5cf | |||
| a7f763faf2 | |||
| 4097fe07c5 | |||
| e7b015f53f | |||
| 05c43ed998 | |||
| 5c4f870898 | |||
| 3481d16577 | |||
| 2d827430ee | |||
| 3262b83420 | |||
| ff02868afc | |||
| a2019aceec | |||
| a07949ec8e | |||
| 65d4416f9e | |||
| 0cbb03cd14 | |||
| 32611391a3 | |||
| 07b7250682 | |||
| 31d2aa6716 | |||
| 20d4266e2f | |||
| c9e429760d | |||
| be33c626b0 | |||
| 1a160df396 | |||
| de97c07c63 | |||
| c339dc5b05 | |||
| 3ed237b629 | |||
| 21c288272d | |||
| c4fffba11b | |||
| e4e5a589b1 | |||
| b1b6affb71 | |||
| 1ac307bc42 | |||
| 80c0125d6b | |||
| c959dc351f | |||
| b806f3e85d | |||
| c5e60ba006 | |||
| 55222c5b0e | |||
| ca8ca903b2 | |||
| da6bcc54cf | |||
| adbc27059a | |||
| ce69e052b7 | |||
| f6aecf5c28 | |||
| 41228dce19 | |||
| 356dad1932 | |||
| e2a7965e43 | |||
| 5655241bef | |||
| add7a91d54 | |||
| 8a790cdd7f | |||
| 10dd0f76eb | |||
| 6e20893a0c | |||
| 2276cb0e75 | |||
| 2808f8b73d | |||
| ffc63dc85f | |||
| 4b32b0a71b | |||
| f8cdd6b9ae | |||
| 10e241e7eb | |||
| 88166926fa | |||
| 704e36c280 | |||
| 20ac42a5a3 | |||
| c8745a9ab0 | |||
| 084905c146 | |||
| ddeadc21a0 | |||
| e49dad2889 | |||
| 69f22b9dba | |||
| 48346edde6 | |||
| 89c6ca94fe | |||
| 7db5882ea2 | |||
| b081f8f8d9 |
+1
-1
@@ -1,7 +1,7 @@
|
||||
# Copy this to `.env` and `.env.test` files
|
||||
|
||||
BITDRIFT_API_KEY=
|
||||
SENTRY_AUTH_TOKEN=
|
||||
EXPO_PUBLIC_ENV=development
|
||||
EXPO_PUBLIC_LOG_LEVEL=debug
|
||||
EXPO_PUBLIC_LOG_DEBUG=
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
|
||||
|
||||
@@ -64,6 +64,7 @@ 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
|
||||
|
||||
@@ -76,6 +76,7 @@ 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,6 +69,9 @@ jobs:
|
||||
- name: Lint check
|
||||
run: yarn lint
|
||||
|
||||
- name: Lint lockfile
|
||||
run: yarn lockfile-lint
|
||||
|
||||
- name: Prettier check
|
||||
run: yarn prettier --check .
|
||||
|
||||
@@ -101,6 +104,7 @@ 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
|
||||
@@ -196,6 +200,7 @@ 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
|
||||
@@ -222,7 +227,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 }}
|
||||
@@ -273,6 +278,7 @@ 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
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go tooling
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.23'
|
||||
- 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.22'
|
||||
go-version: '1.23'
|
||||
- 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
|
||||
|
||||
@@ -28,6 +28,8 @@ 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
|
||||
@@ -54,4 +56,4 @@ jobs:
|
||||
run: yarn intl:build
|
||||
- name: Run tests
|
||||
run: |
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test --forceExit
|
||||
NODE_ENV=test yarn test --forceExit
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
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
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22-bullseye AS build-env
|
||||
FROM golang:1.23-bullseye AS build-env
|
||||
|
||||
WORKDIR /usr/src/social-app
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22-bullseye AS build-env
|
||||
FROM golang:1.23-bullseye AS build-env
|
||||
|
||||
WORKDIR /usr/src/social-app
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ build-web-embed: ## Compile web embed bundle, copy to bskyweb/embedr* directorie
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run all tests
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test
|
||||
NODE_ENV=test yarn test
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run style checks and verify syntax
|
||||
|
||||
@@ -26,7 +26,7 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn: "About"
|
||||
- tapOn: "People"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
|
||||
@@ -74,7 +74,6 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn: "Delete List"
|
||||
- tapOn:
|
||||
id: "confirmBtn"
|
||||
- assertVisible: "This list is empty!"
|
||||
|
||||
- tapOn:
|
||||
label: "Create a new curatelist"
|
||||
@@ -93,14 +92,12 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn: "About"
|
||||
- tapOn: "People"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
- tapOn: "About"
|
||||
- tapOn: "People"
|
||||
|
||||
- tapOn:
|
||||
label: "Adds users on curatelists from the list"
|
||||
id: "addUserBtn"
|
||||
- tapOn: "Start adding people"
|
||||
- assertVisible:
|
||||
id: "listAddUserModal"
|
||||
- tapOn:
|
||||
@@ -145,7 +142,7 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eGotoLists"
|
||||
- tapOn: "Good Ppl"
|
||||
|
||||
- tapOn: "About"
|
||||
- tapOn: "People"
|
||||
- assertVisible:
|
||||
label: "Removes users on curatelists from the list"
|
||||
id: "user-bob.test"
|
||||
|
||||
@@ -11,10 +11,8 @@ appId: xyz.blueskyweb.app
|
||||
|
||||
# Pin alice's feed
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
visible: "Open drawer menu"
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "profileCardButton"
|
||||
- tapOn:
|
||||
@@ -32,8 +30,7 @@ appId: xyz.blueskyweb.app
|
||||
text: "alice-favs"
|
||||
|
||||
# Set alice-favs first
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
@@ -44,8 +41,7 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn:
|
||||
label: "Save button"
|
||||
id: "saveChangesBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn: "Go back"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "alice-favs"
|
||||
@@ -54,8 +50,7 @@ appId: xyz.blueskyweb.app
|
||||
text: "Following"
|
||||
|
||||
# Set following first
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
@@ -66,8 +61,7 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn:
|
||||
label: "Save button"
|
||||
id: "saveChangesBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn: "Go back"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "Following"
|
||||
@@ -76,8 +70,7 @@ appId: xyz.blueskyweb.app
|
||||
text: "alice-favs"
|
||||
|
||||
# Remove following
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
@@ -88,8 +81,7 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn:
|
||||
label: "Save button"
|
||||
id: "saveChangesBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn: "Go back"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "alice-favs"
|
||||
|
||||
@@ -27,7 +27,9 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
id: "bottomBarHomeBtn"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- assertNotVisible: "Feeds ✨"
|
||||
|
||||
- tapOn:
|
||||
|
||||
@@ -14,7 +14,7 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eGotoModeration"
|
||||
- tapOn:
|
||||
id: "moderationlistsBtn"
|
||||
- tapOn: "New"
|
||||
- tapOn: "New list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
- inputText: "Muted Users"
|
||||
|
||||
@@ -6,6 +6,8 @@ appId: xyz.blueskyweb.app
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "http://localhost:8081"
|
||||
- waitForAnimationToEnd
|
||||
- extendedWaitUntil:
|
||||
visible: "Continue"
|
||||
- swipe:
|
||||
from: "Bluesky"
|
||||
direction: DOWN
|
||||
|
||||
+9
-2
@@ -15,9 +15,9 @@ module.exports = function (config) {
|
||||
*/
|
||||
const PLATFORM = process.env.EAS_BUILD_PLATFORM
|
||||
|
||||
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
|
||||
const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production'
|
||||
const IS_DEV = !IS_TESTFLIGHT || !IS_PRODUCTION
|
||||
|
||||
const ASSOCIATED_DOMAINS = [
|
||||
'applinks:bsky.app',
|
||||
@@ -50,7 +50,6 @@ module.exports = function (config) {
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
orientation: 'portrait',
|
||||
icon: './assets/app-icons/ios_icon_default_light.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
primaryColor: '#1083fe',
|
||||
@@ -77,6 +76,7 @@ module.exports = function (config) {
|
||||
'ast',
|
||||
'ca',
|
||||
'de',
|
||||
'el',
|
||||
'es',
|
||||
'fi',
|
||||
'fr',
|
||||
@@ -87,10 +87,13 @@ module.exports = function (config) {
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'km',
|
||||
'ko',
|
||||
'ne',
|
||||
'nl',
|
||||
'pl',
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'th',
|
||||
'tr',
|
||||
@@ -222,11 +225,14 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
'react-native-compressor',
|
||||
// TODO: Reenable when the build issue is fixed.
|
||||
// '@bitdrift/react-native',
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||
'./plugins/withAndroidNoJitpackPlugin.js',
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||
'./plugins/withAppDelegateReferrer.js',
|
||||
@@ -340,6 +346,7 @@ module.exports = function (config) {
|
||||
},
|
||||
},
|
||||
],
|
||||
['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z"/></svg>
|
||||
|
After Width: | Height: | Size: 323 B |
+1
-1
@@ -24,7 +24,7 @@ Then build and copy over the big 'ol `bundle.web.js` file:
|
||||
|
||||
### Golang Daemon
|
||||
|
||||
Install golang. We are generally using v1.22+.
|
||||
Install golang. We generally develop against the current stable release of the language, as declared in `go.mod`.
|
||||
|
||||
In this directory (`bskyweb/`):
|
||||
|
||||
|
||||
@@ -235,6 +235,7 @@ 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)
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
module github.com/bluesky-social/social-app/bskyweb
|
||||
|
||||
go 1.22
|
||||
go 1.23
|
||||
|
||||
toolchain go1.22.4
|
||||
toolchain go1.23.4
|
||||
|
||||
require (
|
||||
github.com/bluesky-social/indigo v0.0.0-20240624223826-fd66f93ce141
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<!-- Hello Humans! API docs at https://atproto.com -->
|
||||
|
||||
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2">
|
||||
<link rel="preload" as="font" type="font/woff2" href="{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2" crossorigin>
|
||||
|
||||
<style>
|
||||
/**
|
||||
|
||||
@@ -87,6 +87,12 @@ However, if you're a part of the Bluesky team and want to enable Sentry, fill in
|
||||
|
||||
If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running `yarn ios` or `yarn android` again.
|
||||
|
||||
### Adding bitdrift
|
||||
|
||||
Adding bitdirft is NOT required. You can keep `BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
|
||||
|
||||
However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
|
||||
|
||||
### Adding and Updating Locales
|
||||
|
||||
- `yarn intl:build` -> you will also need to run this anytime `./src/locale/{locale}/messages.po` change
|
||||
|
||||
@@ -5,9 +5,8 @@ import {LogBox} from 'react-native'
|
||||
import {registerRootComponent} from 'expo'
|
||||
|
||||
import App from '#/App'
|
||||
import {IS_TEST} from '#/env'
|
||||
|
||||
if (IS_TEST) {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
LogBox.ignoreAllLogs() // suppress all logs in tests
|
||||
} else {
|
||||
LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine
|
||||
|
||||
@@ -105,3 +105,19 @@ jest.mock('expo-modules-core', () => ({
|
||||
return () => null
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('expo-localization', () => ({
|
||||
getLocales: () => [],
|
||||
}))
|
||||
|
||||
jest.mock('statsig-react-native-expo', () => ({
|
||||
Statsig: {
|
||||
initialize() {},
|
||||
initializeCalled() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('../src/lib/bitdrift', () => ({}))
|
||||
jest.mock('../src/lib/statsig/statsig', () => ({}))
|
||||
|
||||
@@ -6,6 +6,7 @@ module.exports = {
|
||||
'ast',
|
||||
'ca',
|
||||
'de',
|
||||
'el',
|
||||
'en-GB',
|
||||
'es',
|
||||
'fi',
|
||||
@@ -17,10 +18,13 @@ module.exports = {
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'km',
|
||||
'ko',
|
||||
'ne',
|
||||
'nl',
|
||||
'pl',
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'th',
|
||||
'tr',
|
||||
|
||||
+42
-18
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.96.0",
|
||||
"version": "1.96.4",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -54,7 +54,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.13.18",
|
||||
"@atproto/api": "^0.13.21",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -62,19 +62,19 @@
|
||||
"@expo/webpack-config": "^19.0.0",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@floating-ui/react-dom": "^2.0.8",
|
||||
"@formatjs/intl-locale": "^4.0.0",
|
||||
"@formatjs/intl-numberformat": "^8.10.3",
|
||||
"@formatjs/intl-pluralrules": "^5.2.14",
|
||||
"@formatjs/intl-locale": "^4.2.8",
|
||||
"@formatjs/intl-numberformat": "^8.15.1",
|
||||
"@formatjs/intl-pluralrules": "^5.4.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||
"@haileyok/bluesky-video": "0.2.4",
|
||||
"@haileyok/bluesky-video": "0.2.6",
|
||||
"@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.4.1",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
||||
"@radix-ui/react-dismissable-layer": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "2.0.1",
|
||||
"@radix-ui/react-focus-guards": "^1.1.1",
|
||||
@@ -118,7 +118,7 @@
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "~52.0.17",
|
||||
"expo": "~52.0.19",
|
||||
"expo-application": "^6.0.1",
|
||||
"expo-blur": "^14.0.1",
|
||||
"expo-build-properties": "^0.13.1",
|
||||
@@ -126,7 +126,7 @@
|
||||
"expo-clipboard": "^7.0.0",
|
||||
"expo-dev-client": "^5.0.4",
|
||||
"expo-device": "~7.0.1",
|
||||
"expo-file-system": "^18.0.4",
|
||||
"expo-file-system": "^18.0.6",
|
||||
"expo-font": "~13.0.1",
|
||||
"expo-haptics": "^14.0.0",
|
||||
"expo-image": "~2.0.3",
|
||||
@@ -137,13 +137,14 @@
|
||||
"expo-localization": "~16.0.0",
|
||||
"expo-media-library": "~17.0.3",
|
||||
"expo-navigation-bar": "~4.0.4",
|
||||
"expo-notifications": "~0.29.10",
|
||||
"expo-notifications": "~0.29.11",
|
||||
"expo-screen-orientation": "^8.0.2",
|
||||
"expo-sharing": "^13.0.0",
|
||||
"expo-splash-screen": "~0.29.16",
|
||||
"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.9",
|
||||
"expo-updates": "~0.26.10",
|
||||
"expo-web-browser": "~14.0.1",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
@@ -170,7 +171,7 @@
|
||||
"react-native": "0.76.3",
|
||||
"react-native-compressor": "1.10.3",
|
||||
"react-native-date-picker": "^5.0.7",
|
||||
"react-native-drawer-layout": "^4.0.4",
|
||||
"react-native-drawer-layout": "^4.1.1",
|
||||
"react-native-gesture-handler": "2.20.2",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-image-crop-picker": "^0.41.6",
|
||||
@@ -181,7 +182,7 @@
|
||||
"react-native-picker-select": "^9.3.1",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
"react-native-reanimated": "^3.16.3",
|
||||
"react-native-reanimated": "3.17.0-nightly-20241211-17e89ca24",
|
||||
"react-native-root-siblings": "^4.1.1",
|
||||
"react-native-safe-area-context": "4.14.0",
|
||||
"react-native-screens": "~4.3.0",
|
||||
@@ -205,13 +206,12 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.64",
|
||||
"@atproto/dev-env": "^0.3.67",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@did-plc/server": "^0.0.1",
|
||||
"@expo/config-plugins": "9.0.10",
|
||||
"@expo/prebuild-config": "8.0.22",
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
@@ -250,6 +250,7 @@
|
||||
"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",
|
||||
@@ -266,8 +267,8 @@
|
||||
"@react-native/normalize-colors": "0.76.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@types/react": "^18",
|
||||
"**/expo-constants": "16.0.1",
|
||||
"**/expo-device": "6.0.2",
|
||||
"**/expo-constants": "17.0.3",
|
||||
"**/expo-device": "7.0.1",
|
||||
"**/zod": "3.23.8"
|
||||
},
|
||||
"jest": {
|
||||
@@ -329,5 +330,28 @@
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
diff --git a/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts b/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts
|
||||
new file mode 100644
|
||||
index 0000000..babbb3b
|
||||
--- /dev/null
|
||||
+++ b/node_modules/expo-image-manipulator/src/ImageManipulator.web.ts
|
||||
@@ -0,0 +1,60 @@
|
||||
+import { useReleasingSharedObject } from 'expo-modules-core';
|
||||
+import { SharedRef } from 'expo-modules-core/types';
|
||||
+
|
||||
+import { Action, ImageResult, SaveFormat, SaveOptions } from './ImageManipulator.types';
|
||||
+import { ImageManipulatorContext } from './ImageManipulatorContext';
|
||||
+import ExpoImageManipulator from './NativeImageManipulatorModule';
|
||||
+import { validateArguments } from './validators';
|
||||
+
|
||||
+// @needsAudit
|
||||
+/**
|
||||
+ * Manipulate the image provided via `uri`. Available modifications are rotating, flipping (mirroring),
|
||||
+ * resizing and cropping. Each invocation results in a new file. With one invocation you can provide
|
||||
+ * a set of actions to perform over the image. Overwriting the source file would not have an effect
|
||||
+ * in displaying the result as images are cached.
|
||||
+ * @param uri URI of the file to manipulate. Should be on the local file system or a base64 data URI.
|
||||
+ * @param actions An array of objects representing manipulation options. Each object should have
|
||||
+ * __only one__ of the keys that corresponds to specific transformation.
|
||||
+ * @param saveOptions A map defining how modified image should be saved.
|
||||
+ * @return Promise which fulfils with [`ImageResult`](#imageresult) object.
|
||||
+ * @deprecated It has been replaced by the new, contextual and object-oriented API.
|
||||
+ * Use [`ImageManipulator.manipulate`](#manipulateuri) or [`useImageManipulator`](#useimagemanipulatoruri) instead.
|
||||
+ */
|
||||
+export async function manipulateAsync(
|
||||
+ uri: string,
|
||||
+ actions: Action[] = [],
|
||||
+ saveOptions: SaveOptions = {}
|
||||
+): Promise<ImageResult> {
|
||||
+ validateArguments(uri, actions, saveOptions);
|
||||
+
|
||||
+ const { format = SaveFormat.JPEG, ...rest } = saveOptions;
|
||||
+ const context = ExpoImageManipulator.manipulate(uri);
|
||||
+
|
||||
+ for (const action of actions) {
|
||||
+ if ('resize' in action) {
|
||||
+ context.resize(action.resize);
|
||||
+ } else if ('rotate' in action) {
|
||||
+ context.rotate(action.rotate);
|
||||
+ } else if ('flip' in action) {
|
||||
+ context.flip(action.flip);
|
||||
+ } else if ('crop' in action) {
|
||||
+ context.crop(action.crop);
|
||||
+ } else if ('extent' in action && context.extent) {
|
||||
+ context.extent(action.extent);
|
||||
+ }
|
||||
+ }
|
||||
+ const image = await context.renderAsync(saveOptions.compress);
|
||||
+ const result = await image.saveAsync({ format, ...rest });
|
||||
+
|
||||
+ // These shared objects will not be used anymore, so free up some memory.
|
||||
+ context.release();
|
||||
+ image.release();
|
||||
+
|
||||
+ return result;
|
||||
+}
|
||||
+
|
||||
+export function useImageManipulator(source: string | SharedRef<'image'>): ImageManipulatorContext {
|
||||
+ return useReleasingSharedObject(() => ExpoImageManipulator.manipulate(source), [source]);
|
||||
+}
|
||||
+
|
||||
+export { ExpoImageManipulator as ImageManipulator };
|
||||
diff --git a/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts b/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts
|
||||
index 120d8d3..f8aa49c 100644
|
||||
--- a/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts
|
||||
+++ b/node_modules/expo-image-manipulator/src/ImageManipulatorContext.ts
|
||||
@@ -52,7 +52,7 @@ export declare class ImageManipulatorContext extends SharedObject {
|
||||
/**
|
||||
* Awaits for all manipulation tasks to finish and resolves with a reference to the resulted native image.
|
||||
*/
|
||||
- renderAsync(): Promise<ImageRef>;
|
||||
+ renderAsync(compress?: number): Promise<ImageRef>;
|
||||
}
|
||||
|
||||
export default ExpoImageManipulator.Context as typeof ImageManipulatorContext;
|
||||
diff --git a/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts b/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts
|
||||
index 428848c..363a57a 100644
|
||||
--- a/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts
|
||||
+++ b/node_modules/expo-image-manipulator/src/web/ImageManipulatorContext.web.ts
|
||||
@@ -41,7 +41,7 @@ export default class ImageManipulatorContext extends SharedObject {
|
||||
return this;
|
||||
}
|
||||
|
||||
- async renderAsync(): Promise<ImageManipulatorImageRef> {
|
||||
+ async renderAsync(compress?: number): Promise<ImageManipulatorImageRef> {
|
||||
const canvas = await this.currentTask;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
@@ -49,7 +49,7 @@ export default class ImageManipulatorContext extends SharedObject {
|
||||
const url = blob ? URL.createObjectURL(blob) : canvas.toDataURL();
|
||||
|
||||
resolve(new ImageManipulatorImageRef(url, canvas.width, canvas.height));
|
||||
- });
|
||||
+ }, typeof compress === 'number' ? 'image/jpeg' : undefined, compress);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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,3 @@
|
||||
## expo-modules-core Patch
|
||||
|
||||
This patch fixes an issue where bitdrift's API stream gets blocked by the Expo interceptor used to power the devtools
|
||||
@@ -0,0 +1,15 @@
|
||||
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
index 47c4d15..afe138d 100644
|
||||
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
|
||||
@@ -125,6 +125,10 @@ internal fun peekResponseBody(
|
||||
}
|
||||
|
||||
internal fun shouldParseBody(response: Response): Boolean {
|
||||
+ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") {
|
||||
+ return false
|
||||
+ }
|
||||
+
|
||||
// Check for Content-Type
|
||||
val skipContentTypes = listOf(
|
||||
"text/event-stream", // Server Sent Events
|
||||
@@ -222,7 +222,7 @@ index 40aaf9c..1c60164 100644
|
||||
{
|
||||
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
|
||||
index e9ce48c..ccd9ad6 100644
|
||||
index e9ce48c..84a6fca 100644
|
||||
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
|
||||
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
|
||||
@@ -159,26 +159,8 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
|
||||
@@ -329,3 +329,38 @@ index e9ce48c..ccd9ad6 100644
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1055,6 +1082,22 @@ -(type)getter \
|
||||
RCT_SET_AND_PRESERVE_OFFSET(setShowsVerticalScrollIndicator, showsVerticalScrollIndicator, BOOL)
|
||||
RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat);
|
||||
|
||||
+- (void)setScrollIndicatorInsets:(UIEdgeInsets)value
|
||||
+{
|
||||
+ [_scrollView setScrollIndicatorInsets:value];
|
||||
+}
|
||||
+
|
||||
+- (UIEdgeInsets)scrollIndicatorInsets
|
||||
+{
|
||||
+ UIEdgeInsets verticalScrollIndicatorInsets = [_scrollView verticalScrollIndicatorInsets];
|
||||
+ UIEdgeInsets horizontalScrollIndicatorInsets = [_scrollView horizontalScrollIndicatorInsets];
|
||||
+ return UIEdgeInsetsMake(
|
||||
+ verticalScrollIndicatorInsets.top,
|
||||
+ horizontalScrollIndicatorInsets.left,
|
||||
+ verticalScrollIndicatorInsets.bottom,
|
||||
+ horizontalScrollIndicatorInsets.right);
|
||||
+}
|
||||
+
|
||||
- (void)setAutomaticallyAdjustsScrollIndicatorInsets:(BOOL)automaticallyAdjusts API_AVAILABLE(ios(13.0))
|
||||
{
|
||||
// `automaticallyAdjustsScrollIndicatorInsets` is available since iOS 13.
|
||||
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
index cd1e7eb..c1d0172 100644
|
||||
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
@@ -83,6 +83,7 @@ - (UIView *)view
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval)
|
||||
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
||||
+RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(verticalScrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
|
||||
|
||||
@@ -1,876 +0,0 @@
|
||||
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>
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
+32
-22
@@ -1,5 +1,6 @@
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import '#/lib/sentry' // must be near top
|
||||
import '#/lib/bitdrift' // must be near top
|
||||
import '#/view/icons'
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
@@ -9,6 +10,7 @@ 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'
|
||||
@@ -16,16 +18,12 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {
|
||||
initialize,
|
||||
Provider as StatsigProvider,
|
||||
tryFetchGates,
|
||||
} from '#/lib/statsig/statsig'
|
||||
import {Provider as StatsigProvider, tryFetchGates} from '#/lib/statsig/statsig'
|
||||
import {s} from '#/lib/styles'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {isAndroid, 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'
|
||||
@@ -35,6 +33,7 @@ import {
|
||||
ensureGeolocationResolved,
|
||||
Provider as GeolocationProvider,
|
||||
} from '#/state/geolocation'
|
||||
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -53,11 +52,13 @@ import {
|
||||
import {readLastActiveAccount} from '#/state/session/util'
|
||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||
import {Provider as ComposerProvider} from '#/state/shell/composer'
|
||||
import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar'
|
||||
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {Provider as TrendingConfigProvider} from '#/state/trending-config'
|
||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -77,6 +78,10 @@ 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
|
||||
@@ -99,7 +104,6 @@ function InnerApp() {
|
||||
if (account) {
|
||||
await resumeSession(account)
|
||||
} else {
|
||||
await initialize()
|
||||
await tryFetchGates(undefined, 'prefer-fresh-gates')
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -140,20 +144,24 @@ function InnerApp() {
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<TrendingConfigProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</GestureHandlerRootView>
|
||||
</TrendingConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
@@ -208,7 +216,9 @@ function App() {
|
||||
<SafeAreaProvider
|
||||
initialMetrics={initialWindowMetrics}>
|
||||
<IntentDialogProvider>
|
||||
<InnerApp />
|
||||
<LightStatusBarProvider>
|
||||
<InnerApp />
|
||||
</LightStatusBarProvider>
|
||||
</IntentDialogProvider>
|
||||
</SafeAreaProvider>
|
||||
</StarterPackProvider>
|
||||
|
||||
+22
-13
@@ -22,6 +22,7 @@ import {
|
||||
ensureGeolocationResolved,
|
||||
Provider as GeolocationProvider,
|
||||
} from '#/state/geolocation'
|
||||
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -40,11 +41,13 @@ import {
|
||||
import {readLastActiveAccount} from '#/state/session/util'
|
||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||
import {Provider as ComposerProvider} from '#/state/shell/composer'
|
||||
import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar'
|
||||
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {Provider as TrendingConfigProvider} from '#/state/trending-config'
|
||||
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -119,18 +122,22 @@ function InnerApp() {
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
<HomeBadgeProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<TrendingConfigProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</TrendingConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HomeBadgeProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
@@ -181,7 +188,9 @@ function App() {
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<IntentDialogProvider>
|
||||
<InnerApp />
|
||||
<LightStatusBarProvider>
|
||||
<InnerApp />
|
||||
</LightStatusBarProvider>
|
||||
</IntentDialogProvider>
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
|
||||
+9
-3
@@ -55,10 +55,7 @@ import {NotificationsScreen} from '#/view/screens/Notifications'
|
||||
import {PostThreadScreen} from '#/view/screens/PostThread'
|
||||
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
|
||||
import {ProfileScreen} from '#/view/screens/Profile'
|
||||
import {ProfileFeedScreen} from '#/view/screens/ProfileFeed'
|
||||
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
|
||||
import {ProfileFollowersScreen} from '#/view/screens/ProfileFollowers'
|
||||
import {ProfileFollowsScreen} from '#/view/screens/ProfileFollows'
|
||||
import {ProfileListScreen} from '#/view/screens/ProfileList'
|
||||
import {SavedFeeds} from '#/view/screens/SavedFeeds'
|
||||
import {SearchScreen} from '#/view/screens/Search'
|
||||
@@ -77,6 +74,9 @@ import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
|
||||
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
|
||||
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
|
||||
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||
import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'
|
||||
import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
|
||||
import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
|
||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
||||
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
|
||||
@@ -100,6 +100,7 @@ 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>()
|
||||
|
||||
@@ -376,6 +377,11 @@ 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}
|
||||
|
||||
@@ -158,6 +158,9 @@ export const atoms = {
|
||||
flex_wrap: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
flex_nowrap: {
|
||||
flexWrap: 'nowrap',
|
||||
},
|
||||
flex_0: {
|
||||
flex: web('0 0 auto') || (native(0) as number),
|
||||
},
|
||||
@@ -302,6 +305,18 @@ export const atoms = {
|
||||
border_0: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
border_t_0: {
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
border_b_0: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
border_l_0: {
|
||||
borderLeftWidth: 0,
|
||||
},
|
||||
border_r_0: {
|
||||
borderRightWidth: 0,
|
||||
},
|
||||
border: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
|
||||
@@ -60,6 +60,7 @@ export function createThemes({
|
||||
dim: Theme
|
||||
} {
|
||||
const color = {
|
||||
like: '#ec4899',
|
||||
trueBlack: '#000000',
|
||||
|
||||
gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`,
|
||||
@@ -124,6 +125,7 @@ export function createThemes({
|
||||
const lightPalette = {
|
||||
white: color.gray_0,
|
||||
black: color.gray_1000,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: color.gray_25,
|
||||
contrast_50: color.gray_50,
|
||||
@@ -185,6 +187,7 @@ export function createThemes({
|
||||
const darkPalette: Palette = {
|
||||
white: color.gray_25,
|
||||
black: color.trueBlack,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: color.gray_975,
|
||||
contrast_50: color.gray_950,
|
||||
@@ -246,6 +249,7 @@ export function createThemes({
|
||||
const dimPalette: Palette = {
|
||||
...darkPalette,
|
||||
black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`,
|
||||
like: color.like,
|
||||
|
||||
contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`,
|
||||
contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`,
|
||||
|
||||
@@ -12,6 +12,7 @@ export type ThemeName = 'light' | 'dim' | 'dark'
|
||||
export type Palette = {
|
||||
white: string
|
||||
black: string
|
||||
like: string
|
||||
|
||||
contrast_25: string
|
||||
contrast_50: string
|
||||
|
||||
@@ -110,9 +110,7 @@ export function renderChildrenWithEmoji(
|
||||
return child.split(EMOJI).map((stringPart, index) => [
|
||||
stringPart,
|
||||
emojis[index] ? (
|
||||
<UITextView
|
||||
{...props}
|
||||
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
|
||||
<UITextView {...props} style={[props?.style, {fontFamily: 'System'}]}>
|
||||
{emojis[index]}
|
||||
</UITextView>
|
||||
) : null,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as NavigationBar from 'expo-navigation-bar'
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {Theme} from '../types'
|
||||
@@ -9,10 +10,12 @@ 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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
AtUri,
|
||||
RichText as RichTextApi,
|
||||
} from '@atproto/api'
|
||||
import {msg, plural, Trans} from '@lingui/macro'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -210,10 +210,9 @@ export function Likes({count}: {count: number}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{plural(count || 0, {
|
||||
one: 'Liked by # user',
|
||||
other: 'Liked by # users',
|
||||
})}
|
||||
<Trans>
|
||||
Liked by <Plural value={count || 0} one="# user" other="# users" />
|
||||
</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
@@ -16,7 +15,6 @@ import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {SeenPost} from '#/state/userActionHistory'
|
||||
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
||||
@@ -282,8 +280,8 @@ export function ProfileGrid({
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
logContext="FeedInterstitial"
|
||||
color="secondary_inverted"
|
||||
shape="round"
|
||||
colorInverted
|
||||
/>
|
||||
</ProfileCard.Header>
|
||||
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
||||
@@ -506,23 +504,9 @@ export function SuggestedFeeds() {
|
||||
|
||||
export function ProgressGuide() {
|
||||
const t = useTheme()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const guide = useProgressGuide('like-10-and-follow-7')
|
||||
|
||||
if (isDesktop) {
|
||||
return null
|
||||
}
|
||||
|
||||
return guide ? (
|
||||
<View
|
||||
style={[
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
a.px_lg,
|
||||
a.py_lg,
|
||||
a.pb_lg,
|
||||
]}>
|
||||
return (
|
||||
<View style={[t.atoms.border_contrast_low, a.px_lg, a.py_lg, a.pb_lg]}>
|
||||
<ProgressGuideList />
|
||||
</View>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {atoms as a, tokens} from '#/alf'
|
||||
import {atoms as a, tokens, ViewStyleProp} from '#/alf'
|
||||
|
||||
export function GradientFill({
|
||||
gradient,
|
||||
}: {
|
||||
style,
|
||||
}: ViewStyleProp & {
|
||||
gradient:
|
||||
| typeof tokens.gradients.primary
|
||||
| typeof tokens.gradients.sky
|
||||
| typeof tokens.gradients.midnight
|
||||
| typeof tokens.gradients.sunrise
|
||||
@@ -26,7 +28,7 @@ export function GradientFill({
|
||||
}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
style={[a.absolute, a.inset_0, style]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export function RegionalNotice() {
|
||||
)
|
||||
}
|
||||
|
||||
export function LikeCount({count}: {count: number}) {
|
||||
export function LikeCount({likeCount}: {likeCount: number}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text
|
||||
@@ -93,7 +93,9 @@ export function LikeCount({count}: {count: number}) {
|
||||
t.atoms.text_contrast_medium,
|
||||
{fontWeight: '600'},
|
||||
]}>
|
||||
<Plural value={count} one="Liked by # user" other="Liked by # users" />
|
||||
<Trans>
|
||||
Liked by <Plural value={likeCount} one="# user" other="# users" />
|
||||
</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -138,7 +140,7 @@ export function Default({
|
||||
value={labeler.creator.description}
|
||||
handle={labeler.creator.handle}
|
||||
/>
|
||||
{labeler.likeCount ? <LikeCount count={labeler.likeCount} /> : null}
|
||||
{labeler.likeCount ? <LikeCount likeCount={labeler.likeCount} /> : null}
|
||||
</Content>
|
||||
</Outer>
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useBreakpoints,
|
||||
useGutters,
|
||||
useTheme,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
||||
@@ -29,9 +30,13 @@ 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'])
|
||||
@@ -40,16 +45,18 @@ export function Outer({
|
||||
|
||||
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_sm, a.pt_xs],
|
||||
web: [a.py_sm],
|
||||
native: [a.pb_xs, {minHeight: 48}],
|
||||
web: [a.py_xs, {minHeight: 52}],
|
||||
}),
|
||||
t.atoms.border_contrast_low,
|
||||
gtMobile && [a.mx_auto, {maxWidth: 600}],
|
||||
@@ -85,17 +92,7 @@ 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>) {
|
||||
@@ -175,7 +172,8 @@ export function TitleText({
|
||||
gtMobile && a.text_xl,
|
||||
style,
|
||||
]}
|
||||
numberOfLines={2}>
|
||||
numberOfLines={2}
|
||||
emoji>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ export * as Header from '#/components/Layout/Header'
|
||||
|
||||
export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
noInsetTop?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,6 +29,7 @@ export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||
*/
|
||||
export const Screen = React.memo(function Screen({
|
||||
style,
|
||||
noInsetTop,
|
||||
...props
|
||||
}: ScreenProps) {
|
||||
const {top} = useSafeAreaInsets()
|
||||
@@ -35,7 +37,7 @@ export const Screen = React.memo(function Screen({
|
||||
<>
|
||||
{isWeb && <WebCenterBorders />}
|
||||
<View
|
||||
style={[a.util_screen_outer, {paddingTop: top}, style]}
|
||||
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -10,3 +10,23 @@ export const Context = React.createContext<ContextType>({
|
||||
export const ItemContext = React.createContext<ItemContextType>({
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
export function useMenuContext() {
|
||||
const context = React.useContext(Context)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useMenuContext must be used within a Context.Provider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export function useMenuItemContext() {
|
||||
const context = React.useContext(ItemContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useMenuItemContext must be used within a Context.Provider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -4,12 +4,17 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import flattenReactChildren from 'react-keyed-flatten-children'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {isAndroid, isIOS, isNative} from '#/platform/detection'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Context, ItemContext} from '#/components/Menu/context'
|
||||
import {
|
||||
Context,
|
||||
ItemContext,
|
||||
useMenuContext,
|
||||
useMenuItemContext,
|
||||
} from '#/components/Menu/context'
|
||||
import {
|
||||
ContextType,
|
||||
GroupProps,
|
||||
@@ -25,10 +30,6 @@ export {
|
||||
useDialogControl as useMenuControl,
|
||||
} from '#/components/Dialog'
|
||||
|
||||
export function useMemoControlContext() {
|
||||
return React.useContext(Context)
|
||||
}
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
control,
|
||||
@@ -47,7 +48,7 @@ export function Root({
|
||||
}
|
||||
|
||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
const {control} = React.useContext(Context)
|
||||
const context = useMenuContext()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const {
|
||||
state: pressed,
|
||||
@@ -57,14 +58,14 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
|
||||
return children({
|
||||
isNative: true,
|
||||
control,
|
||||
control: context.control,
|
||||
state: {
|
||||
hovered: false,
|
||||
focused,
|
||||
pressed,
|
||||
},
|
||||
props: {
|
||||
onPress: control.open,
|
||||
onPress: context.control.open,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onPressIn,
|
||||
@@ -82,7 +83,7 @@ export function Outer({
|
||||
showCancel?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}>) {
|
||||
const context = React.useContext(Context)
|
||||
const context = useMenuContext()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
@@ -105,7 +106,7 @@ export function Outer({
|
||||
|
||||
export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
const t = useTheme()
|
||||
const {control} = React.useContext(Context)
|
||||
const context = useMenuContext()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const {
|
||||
state: pressed,
|
||||
@@ -121,9 +122,20 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onPress={async e => {
|
||||
await onPress(e)
|
||||
if (!e.defaultPrevented) {
|
||||
control?.close()
|
||||
if (isAndroid) {
|
||||
/**
|
||||
* Below fix for iOS doesn't work for Android, this does.
|
||||
*/
|
||||
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 => {
|
||||
@@ -156,7 +168,7 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
|
||||
export function ItemText({children, style}: ItemTextProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
const {disabled} = useMenuItemContext()
|
||||
return (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
@@ -177,7 +189,7 @@ export function ItemText({children, style}: ItemTextProps) {
|
||||
|
||||
export function ItemIcon({icon: Comp}: ItemIconProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
const {disabled} = useMenuItemContext()
|
||||
return (
|
||||
<Comp
|
||||
size="lg"
|
||||
@@ -190,6 +202,55 @@ 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 (
|
||||
@@ -223,7 +284,7 @@ export function Group({children, style}: GroupProps) {
|
||||
|
||||
function Cancel() {
|
||||
const {_} = useLingui()
|
||||
const {control} = React.useContext(Context)
|
||||
const context = useMenuContext()
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -231,7 +292,7 @@ function Cancel() {
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onPress={() => control.close()}>
|
||||
onPress={() => context.control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
|
||||
@@ -7,7 +7,12 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import {atoms as a, flatten, useTheme, web} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Context, ItemContext} from '#/components/Menu/context'
|
||||
import {
|
||||
Context,
|
||||
ItemContext,
|
||||
useMenuContext,
|
||||
useMenuItemContext,
|
||||
} from '#/components/Menu/context'
|
||||
import {
|
||||
ContextType,
|
||||
GroupProps,
|
||||
@@ -40,10 +45,6 @@ export function useMenuControl(): Dialog.DialogControlProps {
|
||||
)
|
||||
}
|
||||
|
||||
export function useMemoControlContext() {
|
||||
return React.useContext(Context)
|
||||
}
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
control,
|
||||
@@ -110,7 +111,7 @@ const RadixTriggerPassThrough = React.forwardRef(
|
||||
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
|
||||
|
||||
export function Trigger({children, label, role = 'button'}: TriggerProps) {
|
||||
const {control} = React.useContext(Context)
|
||||
const {control} = useMenuContext()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onMouseEnter,
|
||||
@@ -203,7 +204,7 @@ export function Outer({
|
||||
|
||||
export function Item({children, label, onPress, ...rest}: ItemProps) {
|
||||
const t = useTheme()
|
||||
const {control} = React.useContext(Context)
|
||||
const {control} = useMenuContext()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onMouseEnter,
|
||||
@@ -262,7 +263,7 @@ export function Item({children, label, onPress, ...rest}: ItemProps) {
|
||||
|
||||
export function ItemText({children, style}: ItemTextProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
const {disabled} = useMenuItemContext()
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
@@ -279,7 +280,7 @@ export function ItemText({children, style}: ItemTextProps) {
|
||||
|
||||
export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
const {disabled} = useMenuItemContext()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -303,6 +304,57 @@ export function ItemIcon({icon: Comp, position = 'left'}: 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,
|
||||
a.pt_md,
|
||||
a.pb_sm,
|
||||
t.atoms.text_contrast_low,
|
||||
{
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function Group({children}: GroupProps) {
|
||||
return children
|
||||
}
|
||||
|
||||
@@ -285,6 +285,7 @@ export type FollowButtonProps = {
|
||||
moderationOpts: ModerationOpts
|
||||
logContext: LogEvents['profile:follow']['logContext'] &
|
||||
LogEvents['profile:unfollow']['logContext']
|
||||
colorInverted?: boolean
|
||||
} & Partial<ButtonProps>
|
||||
|
||||
export function FollowButton(props: FollowButtonProps) {
|
||||
@@ -297,6 +298,8 @@ export function FollowButtonInner({
|
||||
profile: profileUnshadowed,
|
||||
moderationOpts,
|
||||
logContext,
|
||||
onPress: onPressProp,
|
||||
colorInverted,
|
||||
...rest
|
||||
}: FollowButtonProps) {
|
||||
const {_} = useLingui()
|
||||
@@ -321,6 +324,7 @@ export function FollowButtonInner({
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
onPressProp?.(e)
|
||||
} catch (err: any) {
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
|
||||
@@ -341,6 +345,7 @@ export function FollowButtonInner({
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
onPressProp?.(e)
|
||||
} catch (err: any) {
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
|
||||
@@ -387,7 +392,7 @@ export function FollowButtonInner({
|
||||
label={followLabel}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
color={colorInverted ? 'secondary_inverted' : 'primary'}
|
||||
{...rest}
|
||||
onPress={onPressFollow}>
|
||||
<ButtonIcon icon={Plus} position={isRound ? undefined : 'left'} />
|
||||
|
||||
@@ -0,0 +1,833 @@
|
||||
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {ScrollView, TextInput, useWindowDimensions, View} from 'react-native'
|
||||
import Animated, {
|
||||
LayoutAnimationConfig,
|
||||
LinearTransition,
|
||||
ZoomInEasyDown,
|
||||
} from 'react-native-reanimated'
|
||||
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useActorSearchPaginated} from '#/state/queries/actor-search'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import {Follow10ProgressGuide} from '#/state/shell/progress-guide'
|
||||
import {ListMethods} from '#/view/com/util/List'
|
||||
import {
|
||||
popularInterests,
|
||||
useInterestsDisplayNames,
|
||||
} from '#/screens/Onboarding/state'
|
||||
import {
|
||||
atoms as a,
|
||||
native,
|
||||
tokens,
|
||||
useBreakpoints,
|
||||
useTheme,
|
||||
ViewStyleProp,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2'
|
||||
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ListFooter} from '../Lists'
|
||||
import {ProgressGuideTask} from './Task'
|
||||
|
||||
type Item =
|
||||
| {
|
||||
type: 'profile'
|
||||
key: string
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
isSuggestion: boolean
|
||||
}
|
||||
| {
|
||||
type: 'empty'
|
||||
key: string
|
||||
message: string
|
||||
}
|
||||
| {
|
||||
type: 'placeholder'
|
||||
key: string
|
||||
}
|
||||
| {
|
||||
type: 'error'
|
||||
key: string
|
||||
}
|
||||
|
||||
export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) {
|
||||
const {_} = useLingui()
|
||||
const control = Dialog.useDialogControl()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {height: minHeight} = useWindowDimensions()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Find people to follow`)}
|
||||
onPress={() => {
|
||||
control.open()
|
||||
logEvent('progressGuide:followDialog:open', {})
|
||||
}}
|
||||
size={gtMobile ? 'small' : 'large'}
|
||||
color="primary"
|
||||
variant="solid">
|
||||
<ButtonIcon icon={PersonGroupIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Find people to follow</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Dialog.Outer control={control} nativeOptions={{minHeight}}>
|
||||
<Dialog.Handle />
|
||||
<DialogInner guide={guide} />
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Fine to keep this top-level.
|
||||
let lastSelectedInterest = ''
|
||||
let lastSearchText = ''
|
||||
|
||||
function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
|
||||
const {_} = useLingui()
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const personalizedInterests = preferences?.interests?.tags
|
||||
const interests = Object.keys(interestsDisplayNames)
|
||||
.sort(boostInterests(popularInterests))
|
||||
.sort(boostInterests(personalizedInterests))
|
||||
const [selectedInterest, setSelectedInterest] = useState(
|
||||
() =>
|
||||
lastSelectedInterest ||
|
||||
(personalizedInterests && interests.includes(personalizedInterests[0])
|
||||
? personalizedInterests[0]
|
||||
: interests[0]),
|
||||
)
|
||||
const [searchText, setSearchText] = useState(lastSearchText)
|
||||
const moderationOpts = useModerationOpts()
|
||||
const listRef = useRef<ListMethods>(null)
|
||||
const inputRef = useRef<TextInput>(null)
|
||||
const [headerHeight, setHeaderHeight] = useState(0)
|
||||
const {currentAccount} = useSession()
|
||||
const [suggestedAccounts, setSuggestedAccounts] = useState<
|
||||
Map<string, AppBskyActorDefs.ProfileView[]>
|
||||
>(() => new Map())
|
||||
|
||||
useEffect(() => {
|
||||
lastSearchText = searchText
|
||||
lastSelectedInterest = selectedInterest
|
||||
}, [searchText, selectedInterest])
|
||||
|
||||
const query = searchText || selectedInterest
|
||||
const {
|
||||
data: searchResults,
|
||||
isFetching,
|
||||
error,
|
||||
isError,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
} = useActorSearchPaginated({
|
||||
query,
|
||||
})
|
||||
|
||||
const hasSearchText = !!searchText
|
||||
|
||||
const items = useMemo(() => {
|
||||
const results = searchResults?.pages.flatMap(r => r.actors)
|
||||
let _items: Item[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
if (isError) {
|
||||
_items.push({
|
||||
type: 'empty',
|
||||
key: 'empty',
|
||||
message: _(msg`We're having network issues, try again`),
|
||||
})
|
||||
} else if (results) {
|
||||
// First pass: search results
|
||||
for (const profile of results) {
|
||||
if (profile.did === currentAccount?.did) continue
|
||||
if (profile.viewer?.following) continue
|
||||
// my sincere apologies to Jake Gold - your bio is too keyword-filled and
|
||||
// your page-rank too high, so you're at the top of half the categories -sfn
|
||||
if (
|
||||
!hasSearchText &&
|
||||
profile.did === 'did:plc:tpg43qhh4lw4ksiffs4nbda3' &&
|
||||
// constrain to 'tech'
|
||||
selectedInterest !== 'tech'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
seen.add(profile.did)
|
||||
_items.push({
|
||||
type: 'profile',
|
||||
// Don't share identity across tabs or typing attempts
|
||||
key: query + ':' + profile.did,
|
||||
profile,
|
||||
isSuggestion: false,
|
||||
})
|
||||
}
|
||||
// Second pass: suggestions
|
||||
_items = _items.flatMap(item => {
|
||||
if (item.type !== 'profile') {
|
||||
return item
|
||||
}
|
||||
const suggestions = suggestedAccounts.get(item.profile.did)
|
||||
if (!suggestions) {
|
||||
return item
|
||||
}
|
||||
const itemWithSuggestions = [item]
|
||||
for (const suggested of suggestions) {
|
||||
if (seen.has(suggested.did)) {
|
||||
// Skip search results from previous step or already seen suggestions
|
||||
continue
|
||||
}
|
||||
seen.add(suggested.did)
|
||||
itemWithSuggestions.push({
|
||||
type: 'profile',
|
||||
key: suggested.did,
|
||||
profile: suggested,
|
||||
isSuggestion: true,
|
||||
})
|
||||
if (itemWithSuggestions.length === 1 + 3) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return itemWithSuggestions
|
||||
})
|
||||
} else {
|
||||
const placeholders: Item[] = Array(10)
|
||||
.fill(0)
|
||||
.map((__, i) => ({
|
||||
type: 'placeholder',
|
||||
key: i + '',
|
||||
}))
|
||||
|
||||
_items.push(...placeholders)
|
||||
}
|
||||
|
||||
return _items
|
||||
}, [
|
||||
_,
|
||||
searchResults,
|
||||
isError,
|
||||
currentAccount?.did,
|
||||
hasSearchText,
|
||||
selectedInterest,
|
||||
suggestedAccounts,
|
||||
query,
|
||||
])
|
||||
|
||||
if (searchText && !isFetching && !items.length && !isError) {
|
||||
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
|
||||
}
|
||||
|
||||
const renderItems = useCallback(
|
||||
({item, index}: {item: Item; index: number}) => {
|
||||
switch (item.type) {
|
||||
case 'profile': {
|
||||
return (
|
||||
<FollowProfileCard
|
||||
profile={item.profile}
|
||||
isSuggestion={item.isSuggestion}
|
||||
moderationOpts={moderationOpts!}
|
||||
setSuggestedAccounts={setSuggestedAccounts}
|
||||
noBorder={index === 0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'placeholder': {
|
||||
return <ProfileCardSkeleton key={item.key} />
|
||||
}
|
||||
case 'empty': {
|
||||
return <Empty key={item.key} message={item.message} />
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
[moderationOpts],
|
||||
)
|
||||
|
||||
const onSelectTab = useCallback(
|
||||
(interest: string) => {
|
||||
setSelectedInterest(interest)
|
||||
inputRef.current?.clear()
|
||||
setSearchText('')
|
||||
listRef.current?.scrollToOffset({
|
||||
offset: 0,
|
||||
animated: false,
|
||||
})
|
||||
},
|
||||
[setSelectedInterest, setSearchText],
|
||||
)
|
||||
|
||||
const listHeader = (
|
||||
<Header
|
||||
guide={guide}
|
||||
inputRef={inputRef}
|
||||
listRef={listRef}
|
||||
searchText={searchText}
|
||||
onSelectTab={onSelectTab}
|
||||
setHeaderHeight={setHeaderHeight}
|
||||
setSearchText={setSearchText}
|
||||
interests={interests}
|
||||
selectedInterest={selectedInterest}
|
||||
interestsDisplayNames={interestsDisplayNames}
|
||||
/>
|
||||
)
|
||||
|
||||
const onEndReached = useCallback(async () => {
|
||||
if (isFetchingNextPage || !hasNextPage || isError) return
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more people to follow', {message: err})
|
||||
}
|
||||
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
return (
|
||||
<Dialog.InnerFlatList
|
||||
ref={listRef}
|
||||
data={items}
|
||||
renderItem={renderItems}
|
||||
ListHeaderComponent={listHeader}
|
||||
stickyHeaderIndices={[0]}
|
||||
keyExtractor={(item: Item) => item.key}
|
||||
style={[
|
||||
a.px_0,
|
||||
web([a.py_0, {height: '100vh', maxHeight: 600}]),
|
||||
native({height: '100%'}),
|
||||
]}
|
||||
webInnerContentContainerStyle={a.py_0}
|
||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||
keyboardDismissMode="on-drag"
|
||||
scrollIndicatorInsets={{top: headerHeight}}
|
||||
initialNumToRender={8}
|
||||
maxToRenderPerBatch={8}
|
||||
onEndReached={onEndReached}
|
||||
itemLayoutAnimation={LinearTransition}
|
||||
ListFooterComponent={
|
||||
<ListFooter
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
error={cleanError(error)}
|
||||
onRetry={fetchNextPage}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
let Header = ({
|
||||
guide,
|
||||
inputRef,
|
||||
listRef,
|
||||
searchText,
|
||||
onSelectTab,
|
||||
setHeaderHeight,
|
||||
setSearchText,
|
||||
interests,
|
||||
selectedInterest,
|
||||
interestsDisplayNames,
|
||||
}: {
|
||||
guide: Follow10ProgressGuide
|
||||
inputRef: React.RefObject<TextInput>
|
||||
listRef: React.RefObject<ListMethods>
|
||||
onSelectTab: (v: string) => void
|
||||
searchText: string
|
||||
setHeaderHeight: (v: number) => void
|
||||
setSearchText: (v: string) => void
|
||||
interests: string[]
|
||||
selectedInterest: string
|
||||
interestsDisplayNames: Record<string, string>
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const control = Dialog.useDialogContext()
|
||||
return (
|
||||
<View
|
||||
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
|
||||
style={[
|
||||
a.relative,
|
||||
web(a.pt_lg),
|
||||
native(a.pt_4xl),
|
||||
a.pb_xs,
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<HeaderTop guide={guide} />
|
||||
|
||||
<View style={[web(a.pt_xs), a.pb_xs]}>
|
||||
<SearchInput
|
||||
inputRef={inputRef}
|
||||
defaultValue={searchText}
|
||||
onChangeText={text => {
|
||||
setSearchText(text)
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
}}
|
||||
onEscape={control.close}
|
||||
/>
|
||||
<Tabs
|
||||
onSelectTab={onSelectTab}
|
||||
interests={interests}
|
||||
selectedInterest={selectedInterest}
|
||||
hasSearchText={!!searchText}
|
||||
interestsDisplayNames={interestsDisplayNames}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Header = memo(Header)
|
||||
|
||||
function HeaderTop({guide}: {guide: Follow10ProgressGuide}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const control = Dialog.useDialogContext()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.px_lg,
|
||||
a.relative,
|
||||
a.flex_row,
|
||||
a.justify_between,
|
||||
a.align_center,
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.z_10,
|
||||
a.text_lg,
|
||||
a.font_heavy,
|
||||
a.leading_tight,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Find people to follow</Trans>
|
||||
</Text>
|
||||
<View style={isWeb && {paddingRight: 36}}>
|
||||
<ProgressGuideTask
|
||||
current={guide.numFollows + 1}
|
||||
total={10 + 1}
|
||||
title={`${guide.numFollows} / 10`}
|
||||
tabularNumsTitle
|
||||
/>
|
||||
</View>
|
||||
{isWeb ? (
|
||||
<Button
|
||||
label={_(msg`Close`)}
|
||||
size="small"
|
||||
shape="round"
|
||||
variant={isWeb ? 'ghost' : 'solid'}
|
||||
color="secondary"
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_20,
|
||||
web({right: -4}),
|
||||
native({right: 0}),
|
||||
native({height: 32, width: 32, borderRadius: 16}),
|
||||
]}
|
||||
onPress={() => control.close()}>
|
||||
<ButtonIcon icon={X} size="md" />
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
let Tabs = ({
|
||||
onSelectTab,
|
||||
interests,
|
||||
selectedInterest,
|
||||
hasSearchText,
|
||||
interestsDisplayNames,
|
||||
}: {
|
||||
onSelectTab: (tab: string) => void
|
||||
interests: string[]
|
||||
selectedInterest: string
|
||||
hasSearchText: boolean
|
||||
interestsDisplayNames: Record<string, string>
|
||||
}): React.ReactNode => {
|
||||
const listRef = useRef<ScrollView>(null)
|
||||
const [scrollX, setScrollX] = useState(0)
|
||||
const [totalWidth, setTotalWidth] = useState(0)
|
||||
const pendingTabOffsets = useRef<{x: number; width: number}[]>([])
|
||||
const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([])
|
||||
|
||||
const onInitialLayout = useNonReactiveCallback(() => {
|
||||
const index = interests.indexOf(selectedInterest)
|
||||
scrollIntoViewIfNeeded(index)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (tabOffsets) {
|
||||
onInitialLayout()
|
||||
}
|
||||
}, [tabOffsets, onInitialLayout])
|
||||
|
||||
function scrollIntoViewIfNeeded(index: number) {
|
||||
const btnLayout = tabOffsets[index]
|
||||
if (!btnLayout) return
|
||||
|
||||
const viewportLeftEdge = scrollX
|
||||
const viewportRightEdge = scrollX + totalWidth
|
||||
const shouldScrollToLeftEdge = viewportLeftEdge > btnLayout.x
|
||||
const shouldScrollToRightEdge =
|
||||
viewportRightEdge < btnLayout.x + btnLayout.width
|
||||
|
||||
if (shouldScrollToLeftEdge) {
|
||||
listRef.current?.scrollTo({
|
||||
x: btnLayout.x - tokens.space.lg,
|
||||
animated: true,
|
||||
})
|
||||
} else if (shouldScrollToRightEdge) {
|
||||
listRef.current?.scrollTo({
|
||||
x: btnLayout.x - totalWidth + btnLayout.width + tokens.space.lg,
|
||||
animated: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectTab(index: number) {
|
||||
const tab = interests[index]
|
||||
onSelectTab(tab)
|
||||
scrollIntoViewIfNeeded(index)
|
||||
}
|
||||
|
||||
function handleTabLayout(index: number, x: number, width: number) {
|
||||
if (!tabOffsets.length) {
|
||||
pendingTabOffsets.current[index] = {x, width}
|
||||
if (pendingTabOffsets.current.length === interests.length) {
|
||||
setTabOffsets(pendingTabOffsets.current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={listRef}
|
||||
horizontal
|
||||
contentContainerStyle={[a.gap_sm, a.px_lg]}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate="fast"
|
||||
snapToOffsets={
|
||||
tabOffsets.length === interests.length
|
||||
? tabOffsets.map(o => o.x - tokens.space.xl)
|
||||
: undefined
|
||||
}
|
||||
onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
|
||||
scrollEventThrottle={200} // big throttle
|
||||
onScroll={evt => setScrollX(evt.nativeEvent.contentOffset.x)}>
|
||||
{interests.map((interest, i) => {
|
||||
const active = interest === selectedInterest && !hasSearchText
|
||||
return (
|
||||
<Tab
|
||||
key={interest}
|
||||
onSelectTab={handleSelectTab}
|
||||
active={active}
|
||||
index={i}
|
||||
interest={interest}
|
||||
interestsDisplayName={interestsDisplayNames[interest]}
|
||||
onLayout={handleTabLayout}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
Tabs = memo(Tabs)
|
||||
|
||||
let Tab = ({
|
||||
onSelectTab,
|
||||
interest,
|
||||
active,
|
||||
index,
|
||||
interestsDisplayName,
|
||||
onLayout,
|
||||
}: {
|
||||
onSelectTab: (index: number) => void
|
||||
interest: string
|
||||
active: boolean
|
||||
index: number
|
||||
interestsDisplayName: string
|
||||
onLayout: (index: number, x: number, width: number) => void
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const activeText = active ? _(msg` (active)`) : ''
|
||||
return (
|
||||
<View
|
||||
key={interest}
|
||||
onLayout={e =>
|
||||
onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
|
||||
}>
|
||||
<Button
|
||||
label={_(msg`Search for "${interestsDisplayName}"${activeText}`)}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
color={active ? 'primary' : 'secondary'}
|
||||
size="small"
|
||||
onPress={() => onSelectTab(index)}>
|
||||
<ButtonIcon icon={SearchIcon} />
|
||||
<ButtonText>{interestsDisplayName}</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Tab = memo(Tab)
|
||||
|
||||
let FollowProfileCard = ({
|
||||
profile,
|
||||
moderationOpts,
|
||||
isSuggestion,
|
||||
setSuggestedAccounts,
|
||||
noBorder,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
isSuggestion: boolean
|
||||
setSuggestedAccounts: (
|
||||
updater: (
|
||||
v: Map<string, AppBskyActorDefs.ProfileView[]>,
|
||||
) => Map<string, AppBskyActorDefs.ProfileView[]>,
|
||||
) => void
|
||||
noBorder?: boolean
|
||||
}): React.ReactNode => {
|
||||
const [hasFollowed, setHasFollowed] = useState(false)
|
||||
const followupSuggestion = useSuggestedFollowsByActorQuery({
|
||||
did: profile.did,
|
||||
enabled: hasFollowed,
|
||||
})
|
||||
const candidates = followupSuggestion.data?.suggestions
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Move out of effect.
|
||||
if (hasFollowed && candidates && candidates.length > 0) {
|
||||
setSuggestedAccounts(suggestions => {
|
||||
const newSuggestions = new Map(suggestions)
|
||||
newSuggestions.set(profile.did, candidates)
|
||||
return newSuggestions
|
||||
})
|
||||
}
|
||||
}, [hasFollowed, profile.did, candidates, setSuggestedAccounts])
|
||||
|
||||
return (
|
||||
<LayoutAnimationConfig skipEntering={!isSuggestion}>
|
||||
<Animated.View entering={native(ZoomInEasyDown)}>
|
||||
<FollowProfileCardInner
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
onFollow={() => setHasFollowed(true)}
|
||||
noBorder={noBorder}
|
||||
/>
|
||||
</Animated.View>
|
||||
</LayoutAnimationConfig>
|
||||
)
|
||||
}
|
||||
FollowProfileCard = memo(FollowProfileCard)
|
||||
|
||||
function FollowProfileCardInner({
|
||||
profile,
|
||||
moderationOpts,
|
||||
onFollow,
|
||||
noBorder,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
onFollow?: () => void
|
||||
noBorder?: boolean
|
||||
}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
const t = useTheme()
|
||||
return (
|
||||
<ProfileCard.Link
|
||||
profile={profile}
|
||||
style={[a.flex_1]}
|
||||
onPress={() => control.close()}>
|
||||
{({hovered, pressed}) => (
|
||||
<CardOuter
|
||||
style={[
|
||||
a.flex_1,
|
||||
noBorder && a.border_t_0,
|
||||
(hovered || pressed) && t.atoms.border_contrast_high,
|
||||
]}>
|
||||
<ProfileCard.Outer>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.FollowButton
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
logContext="PostOnboardingFindFollows"
|
||||
shape="round"
|
||||
onPress={onFollow}
|
||||
colorInverted
|
||||
/>
|
||||
</ProfileCard.Header>
|
||||
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
||||
</ProfileCard.Outer>
|
||||
</CardOuter>
|
||||
)}
|
||||
</ProfileCard.Link>
|
||||
)
|
||||
}
|
||||
|
||||
function CardOuter({
|
||||
children,
|
||||
style,
|
||||
}: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.py_md,
|
||||
a.px_lg,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchInput({
|
||||
onChangeText,
|
||||
onEscape,
|
||||
inputRef,
|
||||
defaultValue,
|
||||
}: {
|
||||
onChangeText: (text: string) => void
|
||||
onEscape: () => void
|
||||
inputRef: React.RefObject<TextInput>
|
||||
defaultValue: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onMouseEnter,
|
||||
onOut: onMouseLeave,
|
||||
} = useInteractionState()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const interacted = hovered || focused
|
||||
|
||||
return (
|
||||
<View
|
||||
{...web({
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
})}
|
||||
style={[a.flex_row, a.align_center, a.gap_sm, a.px_lg, a.py_xs]}>
|
||||
<SearchIcon
|
||||
size="md"
|
||||
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
placeholder={_(msg`Search by name or interest`)}
|
||||
defaultValue={defaultValue}
|
||||
onChangeText={onChangeText}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]}
|
||||
placeholderTextColor={t.palette.contrast_500}
|
||||
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
|
||||
returnKeyType="search"
|
||||
clearButtonMode="while-editing"
|
||||
maxLength={50}
|
||||
onKeyPress={({nativeEvent}) => {
|
||||
if (nativeEvent.key === 'Escape') {
|
||||
onEscape()
|
||||
}
|
||||
}}
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
accessibilityLabel={_(msg`Search profiles`)}
|
||||
accessibilityHint={_(msg`Search profiles`)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileCardSkeleton() {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.py_md,
|
||||
a.px_lg,
|
||||
a.gap_md,
|
||||
a.align_center,
|
||||
a.flex_row,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
{width: 42, height: 42},
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={[a.flex_1, a.gap_sm]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
{width: 80, height: 14},
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_xs,
|
||||
{width: 120, height: 10},
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Empty({message}: {message: string}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View style={[a.p_lg, a.py_xl, a.align_center, a.gap_md]}>
|
||||
<Text style={[a.text_sm, a.italic, t.atoms.text_contrast_high]}>
|
||||
{message}
|
||||
</Text>
|
||||
|
||||
<Text style={[a.text_xs, t.atoms.text_contrast_low]}>(╯°□°)╯︵ ┻━┻</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function boostInterests(boosts?: string[]) {
|
||||
return (_a: string, _b: string) => {
|
||||
const indexA = boosts?.indexOf(_a) ?? -1
|
||||
const indexB = boosts?.indexOf(_b) ?? -1
|
||||
const rankA = indexA === -1 ? Infinity : indexA
|
||||
const rankB = indexB === -1 ? Infinity : indexB
|
||||
return rankA - rankB
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,15 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {FollowDialog} from './FollowDialog'
|
||||
import {ProgressGuideTask} from './Task'
|
||||
|
||||
export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const guide = useProgressGuide('like-10-and-follow-7')
|
||||
const followProgressGuide = useProgressGuide('follow-10')
|
||||
const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7')
|
||||
const guide = followProgressGuide || followAndLikeProgressGuide
|
||||
const {endProgressGuide} = useProgressGuideControls()
|
||||
|
||||
if (guide) {
|
||||
@@ -41,18 +44,33 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
|
||||
<ButtonIcon icon={Times} size="sm" />
|
||||
</Button>
|
||||
</View>
|
||||
<ProgressGuideTask
|
||||
current={guide.numLikes + 1}
|
||||
total={10 + 1}
|
||||
title={_(msg`Like 10 posts`)}
|
||||
subtitle={_(msg`Teach our algorithm what you like`)}
|
||||
/>
|
||||
<ProgressGuideTask
|
||||
current={guide.numFollows + 1}
|
||||
total={7 + 1}
|
||||
title={_(msg`Follow 7 accounts`)}
|
||||
subtitle={_(msg`Bluesky is better with friends!`)}
|
||||
/>
|
||||
{guide.guide === 'follow-10' && (
|
||||
<>
|
||||
<ProgressGuideTask
|
||||
current={guide.numFollows + 1}
|
||||
total={10 + 1}
|
||||
title={_(msg`Follow 10 accounts`)}
|
||||
subtitle={_(msg`Bluesky is better with friends!`)}
|
||||
/>
|
||||
<FollowDialog guide={guide} />
|
||||
</>
|
||||
)}
|
||||
{guide.guide === 'like-10-and-follow-7' && (
|
||||
<>
|
||||
<ProgressGuideTask
|
||||
current={guide.numLikes + 1}
|
||||
total={10 + 1}
|
||||
title={_(msg`Like 10 posts`)}
|
||||
subtitle={_(msg`Teach our algorithm what you like`)}
|
||||
/>
|
||||
<ProgressGuideTask
|
||||
current={guide.numFollows + 1}
|
||||
total={7 + 1}
|
||||
title={_(msg`Follow 7 accounts`)}
|
||||
subtitle={_(msg`Bluesky is better with friends!`)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ export function ProgressGuideTask({
|
||||
total,
|
||||
title,
|
||||
subtitle,
|
||||
tabularNumsTitle,
|
||||
}: {
|
||||
current: number
|
||||
total: number
|
||||
title: string
|
||||
subtitle?: string
|
||||
tabularNumsTitle?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -33,8 +35,16 @@ export function ProgressGuideTask({
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={[a.flex_col, a.gap_2xs, {marginTop: -2}]}>
|
||||
<Text style={[a.text_sm, a.font_bold, a.leading_tight]}>{title}</Text>
|
||||
<View style={[a.flex_col, a.gap_2xs, subtitle && {marginTop: -2}]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
tabularNumsTitle && {fontVariant: ['tabular-nums']},
|
||||
]}>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text
|
||||
style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_tight]}>
|
||||
|
||||
@@ -228,7 +228,7 @@ function Empty() {
|
||||
a.justify_between,
|
||||
a.gap_lg,
|
||||
a.shadow_lg,
|
||||
{marginTop: 2},
|
||||
{marginTop: 1},
|
||||
]}>
|
||||
<View style={[a.gap_xs]}>
|
||||
<Text
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||
// import {makeProfileLink} from '#/lib/routes/links'
|
||||
// import {feedUriToHref} from '#/lib/strings/url-helpers'
|
||||
// import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
|
||||
// import {CloseQuote_Filled_Stroke2_Corner0_Rounded as Quote} from '#/components/icons/Quote'
|
||||
// import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import type {TrendingTopic} from '#/state/queries/trending/useTrendingTopics'
|
||||
import {atoms as a, native, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function TrendingTopic({
|
||||
topic: raw,
|
||||
size,
|
||||
style,
|
||||
}: {topic: TrendingTopic; size?: 'large' | 'small'} & ViewStyleProp) {
|
||||
const t = useTheme()
|
||||
const topic = useTopic(raw)
|
||||
|
||||
const isSmall = size === 'small'
|
||||
// const hasAvi = topic.type === 'feed' || topic.type === 'profile'
|
||||
// const aviSize = isSmall ? 16 : 20
|
||||
// const iconSize = isSmall ? 16 : 20
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.rounded_full,
|
||||
a.border,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg,
|
||||
isSmall
|
||||
? [
|
||||
{
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
]
|
||||
: [a.py_sm, a.px_md],
|
||||
style,
|
||||
/*
|
||||
{
|
||||
padding: 6,
|
||||
gap: hasAvi ? 4 : 2,
|
||||
},
|
||||
a.pr_md,
|
||||
*/
|
||||
]}>
|
||||
{/*
|
||||
<View
|
||||
style={[
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.rounded_full,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
width: aviSize,
|
||||
height: aviSize,
|
||||
},
|
||||
]}>
|
||||
{topic.type === 'tag' ? (
|
||||
<Hashtag width={iconSize} />
|
||||
) : topic.type === 'topic' ? (
|
||||
<Quote width={iconSize - 2} />
|
||||
) : topic.type === 'feed' ? (
|
||||
<UserAvatar
|
||||
type="user"
|
||||
size={aviSize}
|
||||
avatar=""
|
||||
/>
|
||||
) : (
|
||||
<UserAvatar
|
||||
type="user"
|
||||
size={aviSize}
|
||||
avatar=""
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
*/}
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
isSmall ? [a.text_sm] : [a.text_md, {paddingBottom: 1}],
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{topic.displayName}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function TrendingTopicSkeleton({
|
||||
size = 'large',
|
||||
index = 0,
|
||||
}: {
|
||||
size?: 'large' | 'small'
|
||||
index?: number
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const isSmall = size === 'small'
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.border,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg_contrast_25,
|
||||
isSmall
|
||||
? {
|
||||
width: index % 2 === 0 ? 75 : 90,
|
||||
height: 27,
|
||||
}
|
||||
: {
|
||||
width: index % 2 === 0 ? 90 : 110,
|
||||
height: 36,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function TrendingTopicLink({
|
||||
topic: raw,
|
||||
children,
|
||||
...rest
|
||||
}: {
|
||||
topic: TrendingTopic
|
||||
} & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const topic = useTopic(raw)
|
||||
|
||||
return (
|
||||
<InternalLink
|
||||
label={topic.label}
|
||||
to={topic.url}
|
||||
PressableComponent={native(PressableScale)}
|
||||
{...rest}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
)
|
||||
}
|
||||
|
||||
type ParsedTrendingTopic =
|
||||
| {
|
||||
type: 'topic' | 'tag' | 'unknown'
|
||||
label: string
|
||||
displayName: string
|
||||
url: string
|
||||
uri: undefined
|
||||
}
|
||||
| {
|
||||
type: 'profile' | 'feed'
|
||||
label: string
|
||||
displayName: string
|
||||
url: string
|
||||
uri: AtUri
|
||||
}
|
||||
|
||||
export function useTopic(raw: TrendingTopic): ParsedTrendingTopic {
|
||||
const {_} = useLingui()
|
||||
return React.useMemo(() => {
|
||||
const {topic: displayName, link} = raw
|
||||
|
||||
if (link.startsWith('/search')) {
|
||||
return {
|
||||
type: 'topic',
|
||||
label: _(msg`Browse posts about ${displayName}`),
|
||||
displayName,
|
||||
uri: undefined,
|
||||
url: link,
|
||||
}
|
||||
} else if (link.startsWith('/hashtag')) {
|
||||
return {
|
||||
type: 'tag',
|
||||
label: _(msg`Browse posts tagged with ${displayName}`),
|
||||
displayName,
|
||||
// displayName: displayName.replace(/^#/, ''),
|
||||
uri: undefined,
|
||||
url: link,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if (!link.startsWith('at://')) {
|
||||
// above logic
|
||||
} else {
|
||||
const urip = new AtUri(link)
|
||||
switch (urip.collection) {
|
||||
case 'app.bsky.actor.profile': {
|
||||
return {
|
||||
type: 'profile',
|
||||
label: _(msg`View ${displayName}'s profile`),
|
||||
displayName,
|
||||
uri: urip,
|
||||
url: makeProfileLink({did: urip.host, handle: urip.host}),
|
||||
}
|
||||
}
|
||||
case 'app.bsky.feed.generator': {
|
||||
return {
|
||||
type: 'feed',
|
||||
label: _(msg`Browse the ${displayName} feed`),
|
||||
displayName,
|
||||
uri: urip,
|
||||
url: feedUriToHref(link),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return {
|
||||
type: 'unknown',
|
||||
label: _(msg`Browse topic ${displayName}`),
|
||||
displayName,
|
||||
uri: undefined,
|
||||
url: link,
|
||||
}
|
||||
}, [_, raw])
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
renderChildrenWithEmoji,
|
||||
TextProps,
|
||||
} from '#/alf/typography'
|
||||
import {IS_DEV} from '#/env'
|
||||
export type {TextProps}
|
||||
|
||||
/**
|
||||
@@ -31,7 +30,7 @@ export function Text({
|
||||
flags,
|
||||
})
|
||||
|
||||
if (IS_DEV) {
|
||||
if (__DEV__) {
|
||||
if (!emoji && childHasEmoji(children)) {
|
||||
logger.warn(
|
||||
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
|
||||
|
||||
@@ -311,7 +311,7 @@ export function PostInteractionSettingsForm({
|
||||
onChange={onChangeQuotesEnabled}
|
||||
style={[a.justify_between, a.pt_xs]}>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Quote posts enabled</Trans>
|
||||
<Trans>Allow quote posts</Trans>
|
||||
</Text>
|
||||
<Toggle.Switch />
|
||||
</Toggle.Item>
|
||||
|
||||
@@ -15,7 +15,6 @@ import {useOnboardingState} from '#/state/shell'
|
||||
* NUXs
|
||||
*/
|
||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
type Context = {
|
||||
activeNux: Nux | undefined
|
||||
@@ -93,10 +92,10 @@ function Inner({
|
||||
setActiveNux(undefined)
|
||||
}, [activeNux, setActiveNux])
|
||||
|
||||
if (IS_DEV && typeof window !== 'undefined') {
|
||||
if (__DEV__ && typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
window.clearNuxDialog = (id: Nux) => {
|
||||
if (!IS_DEV || !id) return
|
||||
if (!__DEV__ || !id) return
|
||||
resetNuxs([id])
|
||||
unsnooze()
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ export function SearchablePeopleList({
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const control = Dialog.useDialogContext()
|
||||
const [headerHeight, setHeaderHeight] = useState(0)
|
||||
const listRef = useRef<ListMethods>(null)
|
||||
const {currentAccount} = useSession()
|
||||
const inputRef = useRef<TextInput>(null)
|
||||
@@ -237,6 +238,7 @@ export function SearchablePeopleList({
|
||||
const listHeader = useMemo(() => {
|
||||
return (
|
||||
<View
|
||||
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
|
||||
style={[
|
||||
a.relative,
|
||||
web(a.pt_lg),
|
||||
@@ -315,6 +317,7 @@ export function SearchablePeopleList({
|
||||
]}
|
||||
webInnerContentContainerStyle={a.py_0}
|
||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||
scrollIndicatorInsets={{top: headerHeight}}
|
||||
keyboardDismissMode="on-drag"
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,15 @@ import {
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {android, atoms as a, TextStyleProp, useTheme, web} from '#/alf'
|
||||
import {
|
||||
android,
|
||||
applyFonts,
|
||||
atoms as a,
|
||||
TextStyleProp,
|
||||
useAlf,
|
||||
useTheme,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -148,6 +156,7 @@ export function createInput(Component: typeof TextInput) {
|
||||
...rest
|
||||
}: InputProps) {
|
||||
const t = useTheme()
|
||||
const {fonts} = useAlf()
|
||||
const ctx = React.useContext(Context)
|
||||
const withinRoot = Boolean(ctx.inputRef)
|
||||
|
||||
@@ -171,6 +180,48 @@ export function createInput(Component: typeof TextInput) {
|
||||
|
||||
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
|
||||
|
||||
const flattened = StyleSheet.flatten([
|
||||
a.relative,
|
||||
a.z_20,
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
t.atoms.text,
|
||||
a.px_xs,
|
||||
{
|
||||
// paddingVertical doesn't work w/multiline - esb
|
||||
paddingTop: 12,
|
||||
paddingBottom: 13,
|
||||
lineHeight: a.text_md.fontSize * 1.1875,
|
||||
textAlignVertical: rest.multiline ? 'top' : undefined,
|
||||
minHeight: rest.multiline ? 80 : undefined,
|
||||
minWidth: 0,
|
||||
},
|
||||
// fix for autofill styles covering border
|
||||
web({
|
||||
paddingTop: 10,
|
||||
paddingBottom: 11,
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
}),
|
||||
android({
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}),
|
||||
style,
|
||||
])
|
||||
|
||||
applyFonts(flattened, fonts.family)
|
||||
|
||||
// should always be defined on `typography`
|
||||
// @ts-ignore
|
||||
if (flattened.fontSize) {
|
||||
// @ts-ignore
|
||||
flattened.fontSize = Math.round(
|
||||
// @ts-ignore
|
||||
flattened.fontSize * fonts.scaleMultiplier,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component
|
||||
@@ -192,35 +243,7 @@ export function createInput(Component: typeof TextInput) {
|
||||
placeholderTextColor={t.palette.contrast_500}
|
||||
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
|
||||
hitSlop={HITSLOP_20}
|
||||
style={[
|
||||
a.relative,
|
||||
a.z_20,
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
t.atoms.text,
|
||||
a.px_xs,
|
||||
{
|
||||
// paddingVertical doesn't work w/multiline - esb
|
||||
paddingTop: 12,
|
||||
paddingBottom: 13,
|
||||
lineHeight: a.text_md.fontSize * 1.1875,
|
||||
textAlignVertical: rest.multiline ? 'top' : undefined,
|
||||
minHeight: rest.multiline ? 80 : undefined,
|
||||
minWidth: 0,
|
||||
},
|
||||
// fix for autofill styles covering border
|
||||
web({
|
||||
paddingTop: 10,
|
||||
paddingBottom: 11,
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
}),
|
||||
android({
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
}),
|
||||
style,
|
||||
]}
|
||||
style={flattened}
|
||||
/>
|
||||
|
||||
<View
|
||||
|
||||
@@ -12,6 +12,7 @@ import {formatDistance, Locale} from 'date-fns'
|
||||
import {
|
||||
ca,
|
||||
de,
|
||||
el,
|
||||
enGB,
|
||||
es,
|
||||
fi,
|
||||
@@ -22,10 +23,12 @@ import {
|
||||
id,
|
||||
it,
|
||||
ja,
|
||||
km,
|
||||
ko,
|
||||
nl,
|
||||
pl,
|
||||
ptBR,
|
||||
ro,
|
||||
ru,
|
||||
th,
|
||||
tr,
|
||||
@@ -48,6 +51,7 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
ast: undefined,
|
||||
ca,
|
||||
de,
|
||||
el,
|
||||
['en-GB']: enGB,
|
||||
es,
|
||||
fi,
|
||||
@@ -59,18 +63,21 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
id,
|
||||
it,
|
||||
ja,
|
||||
km,
|
||||
ko,
|
||||
ne: undefined,
|
||||
nl,
|
||||
pl,
|
||||
['pt-BR']: ptBR,
|
||||
ro,
|
||||
ru,
|
||||
th,
|
||||
tr,
|
||||
uk,
|
||||
vi,
|
||||
['zh-CN']: zhCN,
|
||||
['zh-HK']: zhHK,
|
||||
['zh-TW']: zhTW,
|
||||
['zh-Hans-CN']: zhCN,
|
||||
['zh-Hant-HK']: zhHK,
|
||||
['zh-Hant-TW']: zhTW,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,9 +8,9 @@ export function useHeaderOffset() {
|
||||
if (isDesktop || isTablet) {
|
||||
return 0
|
||||
}
|
||||
const navBarHeight = 42
|
||||
const tabBarPad = 10 + 10 + 6 // padding + arbitrary
|
||||
const navBarHeight = 52
|
||||
const tabBarPad = 10 + 10 + 3 // padding + border
|
||||
const normalLineHeight = 20 // matches tab bar
|
||||
const tabBarText = normalLineHeight * fontScale
|
||||
return navBarHeight + tabBarPad + tabBarText
|
||||
return navBarHeight + tabBarPad + tabBarText - 4 // for some reason, this calculation is wrong by 4 pixels, which we adjust
|
||||
}
|
||||
|
||||
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
|
||||
export const Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z',
|
||||
})
|
||||
|
||||
export const Pin_Filled_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M7.5 2a1 1 0 0 0-1 1v3.997a6.25 6.25 0 0 1-1.83 4.42l-.377.376A1 1 0 0 0 4 12.5V15a1 1 0 0 0 1 1h6v5a1 1 0 1 0 2 0v-5h6a1 1 0 0 0 1-1v-2.5a1 1 0 0 0-.293-.707l-.376-.377a6.25 6.25 0 0 1-1.831-4.42V3.001a1 1 0 0 0-1-1h-9Z',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {
|
||||
useTrendingSettings,
|
||||
useTrendingSettingsApi,
|
||||
} from '#/state/preferences/trending'
|
||||
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
|
||||
import {useTrendingConfig} from '#/state/trending-config'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {TrendingTopicLink} from '#/components/TrendingTopics'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function TrendingInterstitial() {
|
||||
const {enabled} = useTrendingConfig()
|
||||
const {trendingDisabled} = useTrendingSettings()
|
||||
return enabled && !trendingDisabled ? <Inner /> : null
|
||||
}
|
||||
|
||||
export function Inner() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const gutters = useGutters([0, 'base', 0, 'base'])
|
||||
const trendingPrompt = Prompt.usePromptControl()
|
||||
const {setTrendingDisabled} = useTrendingSettingsApi()
|
||||
const {data: trending, error, isLoading} = useTrendingTopics()
|
||||
const noTopics = !isLoading && !error && !trending?.topics?.length
|
||||
|
||||
const onConfirmHide = React.useCallback(() => {
|
||||
logEvent('trendingTopics:hide', {context: 'interstitial'})
|
||||
setTrendingDisabled(true)
|
||||
}, [setTrendingDisabled])
|
||||
|
||||
return error || noTopics ? null : (
|
||||
<View style={[t.atoms.border_contrast_low, a.border_t]}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate="fast">
|
||||
<View style={[gutters, a.flex_row, a.align_center, a.gap_lg]}>
|
||||
<View style={{paddingLeft: 4, paddingRight: 2}}>
|
||||
<Graph size="sm" />
|
||||
</View>
|
||||
{isLoading ? (
|
||||
<View style={[a.py_lg, a.flex_row, a.gap_lg, a.align_center]}>
|
||||
<LoadingPlaceholder
|
||||
width={80}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={50}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={120}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={30}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<LoadingPlaceholder
|
||||
width={180}
|
||||
height={undefined}
|
||||
style={{alignSelf: 'stretch'}}
|
||||
/>
|
||||
<Text
|
||||
style={[t.atoms.text_contrast_medium, a.text_sm, a.font_bold]}>
|
||||
{' '}
|
||||
</Text>
|
||||
</View>
|
||||
) : !trending?.topics ? null : (
|
||||
<>
|
||||
{trending.topics.map(topic => (
|
||||
<TrendingTopicLink
|
||||
key={topic.link}
|
||||
topic={topic}
|
||||
onPress={() => {
|
||||
logEvent('trendingTopic:click', {context: 'interstitial'})
|
||||
}}>
|
||||
<View style={[a.py_lg]}>
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
{opacity: 0.7}, // NOTE: we use opacity 0.7 instead of a color to match the color of the home pager tab bar
|
||||
]}>
|
||||
{topic.topic}
|
||||
</Text>
|
||||
</View>
|
||||
</TrendingTopicLink>
|
||||
))}
|
||||
<Button
|
||||
label={_(msg`Hide trending topics`)}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={() => trendingPrompt.open()}>
|
||||
<ButtonIcon icon={X} />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<Prompt.Basic
|
||||
control={trendingPrompt}
|
||||
title={_(msg`Hide trending topics?`)}
|
||||
description={_(msg`You can update this later from your settings.`)}
|
||||
confirmButtonCta={_(msg`Hide`)}
|
||||
onConfirm={onConfirmHide}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {msg, Plural} from '@lingui/macro'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -50,17 +50,23 @@ export function LabelsOnMe({
|
||||
<ButtonIcon position="left" icon={CircleInfo} />
|
||||
<ButtonText style={[a.leading_snug]}>
|
||||
{type === 'account' ? (
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has been placed on this account"
|
||||
other="# labels have been placed on this account"
|
||||
/>
|
||||
<Trans>
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has"
|
||||
other="# labels have"
|
||||
/>{' '}
|
||||
been placed on this account
|
||||
</Trans>
|
||||
) : (
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has been placed on this content"
|
||||
other="# labels have been placed on this content"
|
||||
/>
|
||||
<Trans>
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has"
|
||||
other="# labels have"
|
||||
/>{' '}
|
||||
been placed on this content
|
||||
</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
export const IS_TEST = process.env.EXPO_PUBLIC_ENV === 'test'
|
||||
export const IS_DEV = __DEV__
|
||||
export const IS_PROD = !IS_DEV
|
||||
export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || ''
|
||||
export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as
|
||||
| 'debug'
|
||||
|
||||
@@ -11,7 +11,10 @@ export async function uploadBlob(
|
||||
input: string | Blob,
|
||||
encoding?: string,
|
||||
): Promise<ComAtprotoRepoUploadBlob.Response> {
|
||||
if (typeof input === 'string' && input.startsWith('data:')) {
|
||||
if (
|
||||
typeof input === 'string' &&
|
||||
(input.startsWith('data:') || input.startsWith('blob:'))
|
||||
) {
|
||||
const blob = await fetch(input).then(r => r.blob())
|
||||
return agent.uploadBlob(blob, {encoding})
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,9 +1,7 @@
|
||||
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
|
||||
|
||||
export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV
|
||||
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||
export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
|
||||
export const IS_INTERNAL = IS_DEV || IS_TESTFLIGHT
|
||||
export const IS_INTERNAL = __DEV__ || IS_TESTFLIGHT
|
||||
|
||||
// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
|
||||
// along with the other version info. Useful for debugging/reporting.
|
||||
@@ -12,9 +10,9 @@ export const BUNDLE_IDENTIFIER = process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? ''
|
||||
// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used
|
||||
// for Statsig reporting and shouldn't be used to identify a specific bundle.
|
||||
export const BUNDLE_DATE =
|
||||
IS_TESTFLIGHT || IS_DEV ? 0 : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
|
||||
IS_TESTFLIGHT || __DEV__ ? 0 : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
|
||||
|
||||
export const appVersion = `${nativeApplicationVersion}.${nativeBuildVersion}`
|
||||
export const bundleInfo = `${BUNDLE_IDENTIFIER} (${
|
||||
IS_DEV ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod'
|
||||
__DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod'
|
||||
})`
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {version} from '../../package.json'
|
||||
|
||||
export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV
|
||||
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||
export const IS_TESTFLIGHT = false
|
||||
export const IS_INTERNAL = IS_DEV
|
||||
export const IS_INTERNAL = __DEV__
|
||||
|
||||
// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
|
||||
// along with the other version info. Useful for debugging/reporting.
|
||||
@@ -12,9 +10,9 @@ export const BUNDLE_IDENTIFIER =
|
||||
|
||||
// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used
|
||||
// for Statsig reporting and shouldn't be used to identify a specific bundle.
|
||||
export const BUNDLE_DATE = IS_DEV
|
||||
export const BUNDLE_DATE = __DEV__
|
||||
? 0
|
||||
: Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
|
||||
|
||||
export const appVersion = version
|
||||
export const bundleInfo = `${BUNDLE_IDENTIFIER} (${IS_DEV ? 'dev' : 'prod'})`
|
||||
export const bundleInfo = `${BUNDLE_IDENTIFIER} (${__DEV__ ? 'dev' : 'prod'})`
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// import {init} from '@bitdrift/react-native'
|
||||
// import {Statsig} from 'statsig-react-native-expo'
|
||||
// export {debug, error, info, warn} from '@bitdrift/react-native'
|
||||
|
||||
// import {initPromise} from './statsig/statsig'
|
||||
|
||||
// const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
|
||||
|
||||
// initPromise.then(() => {
|
||||
// let isEnabled = false
|
||||
// try {
|
||||
// if (Statsig.checkGate('enable_bitdrift')) {
|
||||
// isEnabled = true
|
||||
// }
|
||||
// } catch (e) {
|
||||
// // Statsig may complain about it being called too early.
|
||||
// }
|
||||
// if (isEnabled && BITDRIFT_API_KEY) {
|
||||
// init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'})
|
||||
// }
|
||||
// })
|
||||
|
||||
// TODO: Reenable when the build issue is fixed.
|
||||
export function debug(_message: string) {}
|
||||
export function error(_message: string) {}
|
||||
export function info(_message: string) {}
|
||||
export function warn(_message: string) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
export function debug() {}
|
||||
export function error() {}
|
||||
export function info() {}
|
||||
export function warn() {}
|
||||
+1
-10
@@ -21,16 +21,7 @@ export const STARTER_PACK_MAX_SIZE = 150
|
||||
// code and update this number with each release until we can get the
|
||||
// server route done.
|
||||
// -prf
|
||||
export const JOINED_THIS_WEEK = 2880000 // estimate as of 11/26/24
|
||||
|
||||
export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
|
||||
'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at
|
||||
'did:plc:fpruhuo22xkm5o7ttr2ktxdo': true, // danabra.mov
|
||||
'did:plc:p2cp5gopk7mgjegy6wadk3ep': true, // samuel.bsky.team
|
||||
'did:plc:ragtjsm2j2vknwkz3zp4oxrd': true, // pfrazee.com
|
||||
'did:plc:vpkhqolt662uhesyj6nxm7ys': true, // why.bsky.team
|
||||
'did:plc:3jpt2mvvsumj2r7eqk4gzzjz': true, // esb.lol
|
||||
}
|
||||
export const JOINED_THIS_WEEK = 560000 // estimate as of 12/18/24
|
||||
|
||||
const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
|
||||
export function FEEDBACK_FORM_URL({
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
} from 'react-native-keyboard-controller'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
const KeyboardControllerRefCountContext = createContext<{
|
||||
incrementRefCount: () => void
|
||||
decrementRefCount: () => void
|
||||
@@ -57,7 +55,7 @@ function KeyboardControllerProviderInner({
|
||||
refCount.current--
|
||||
setEnabled(refCount.current > 0)
|
||||
|
||||
if (IS_DEV && refCount.current < 0) {
|
||||
if (__DEV__ && refCount.current < 0) {
|
||||
console.error('KeyboardController ref count < 0')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -239,14 +239,21 @@ export function useNotificationsHandler() {
|
||||
)
|
||||
logEvent('notifications:openApp', {})
|
||||
invalidateCachedUnreadPage()
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
|
||||
const payload = e.notification.request.trigger
|
||||
.payload as NotificationPayload
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||
if (
|
||||
payload.reason === 'mention' ||
|
||||
payload.reason === 'quote' ||
|
||||
payload.reason === 'reply'
|
||||
) {
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||
}
|
||||
logger.debug('Notifications: handleNotification', {
|
||||
content: e.notification.request.content,
|
||||
payload: e.notification.request.trigger.payload,
|
||||
})
|
||||
handleNotification(
|
||||
e.notification.request.trigger.payload as NotificationPayload,
|
||||
)
|
||||
handleNotification(payload)
|
||||
Notifications.dismissAllNotificationsAsync()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export function useNotificationsRegistration() {}
|
||||
|
||||
export function useRequestNotificationsPermission() {
|
||||
return async (
|
||||
_context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home',
|
||||
) => {}
|
||||
}
|
||||
|
||||
export async function decrementBadgeCount(_by: number) {}
|
||||
|
||||
export async function resetBadgeCount() {}
|
||||
@@ -47,6 +47,7 @@ export type CommonNavigatorParams = {
|
||||
AppIconSettings: undefined
|
||||
Search: {q?: string}
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Topic: {topic: string}
|
||||
MessagesConversation: {conversation: string; embed?: string}
|
||||
MessagesSettings: undefined
|
||||
NotificationSettings: undefined
|
||||
@@ -75,7 +76,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
|
||||
}
|
||||
|
||||
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
|
||||
Notifications: {show?: 'all'}
|
||||
Notifications: undefined
|
||||
}
|
||||
|
||||
export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
|
||||
@@ -90,8 +91,9 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
|
||||
Home: undefined
|
||||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
Notifications: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Topic: {topic: string}
|
||||
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||
}
|
||||
|
||||
@@ -102,9 +104,10 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
||||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
NotificationsTab: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
Notifications: undefined
|
||||
MyProfileTab: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Topic: {topic: string}
|
||||
MessagesTab: undefined
|
||||
Messages: {animation?: 'push' | 'pop'}
|
||||
Start: {name: string; rkey: string}
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ import {Platform} from 'react-native'
|
||||
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
|
||||
import {init} from '@sentry/react-native'
|
||||
|
||||
import {BUILD_ENV, IS_DEV, IS_TESTFLIGHT} from '#/lib/app-info'
|
||||
import {IS_TESTFLIGHT} from '#/lib/app-info'
|
||||
|
||||
/**
|
||||
* Examples:
|
||||
@@ -27,14 +27,14 @@ const release = nativeApplicationVersion ?? 'dev'
|
||||
*/
|
||||
const dist = `${Platform.OS}.${nativeBuildVersion}.${
|
||||
IS_TESTFLIGHT ? 'tf' : ''
|
||||
}${IS_DEV ? 'dev' : ''}`
|
||||
}${__DEV__ ? 'dev' : ''}`
|
||||
|
||||
init({
|
||||
enabled: !__DEV__,
|
||||
autoSessionTracking: false,
|
||||
dsn: 'https://05bc3789bf994b81bd7ce20c86ccd3ae@o4505071687041024.ingest.sentry.io/4505071690514432',
|
||||
debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production
|
||||
environment: BUILD_ENV ?? 'development',
|
||||
environment: process.env.NODE_ENV,
|
||||
dist,
|
||||
release,
|
||||
})
|
||||
|
||||
@@ -162,6 +162,7 @@ export type LogEvents = {
|
||||
| 'StarterPackProfilesList'
|
||||
| 'FeedInterstitial'
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'PostOnboardingFindFollows'
|
||||
}
|
||||
'profile:unfollow': {
|
||||
logContext:
|
||||
@@ -177,6 +178,7 @@ export type LogEvents = {
|
||||
| 'StarterPackProfilesList'
|
||||
| 'FeedInterstitial'
|
||||
| 'ProfileHeaderSuggestedFollows'
|
||||
| 'PostOnboardingFindFollows'
|
||||
}
|
||||
'chat:create': {
|
||||
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
|
||||
@@ -234,4 +236,20 @@ export type LogEvents = {
|
||||
'tmd:share': {}
|
||||
'tmd:download': {}
|
||||
'tmd:post': {}
|
||||
|
||||
'trendingTopics:show': {
|
||||
context: 'settings'
|
||||
}
|
||||
'trendingTopics:hide': {
|
||||
context: 'settings' | 'sidebar' | 'interstitial' | 'explore:trending'
|
||||
}
|
||||
'trendingTopic:click': {
|
||||
context: 'sidebar' | 'interstitial' | 'explore'
|
||||
}
|
||||
'recommendedTopic:click': {
|
||||
context: 'explore'
|
||||
}
|
||||
|
||||
'progressGuide:hide': {}
|
||||
'progressGuide:followDialog:open': {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export type Gate =
|
||||
// Keep this alphabetic please.
|
||||
| 'debug_show_feedcontext' // DISABLED DUE TO EME
|
||||
| 'post_feed_lang_window' // DISABLED DUE TO EME
|
||||
| 'debug_show_feedcontext'
|
||||
| 'debug_subscriptions'
|
||||
| 'new_postonboarding'
|
||||
| 'remove_show_latest_button'
|
||||
| 'trending_topics_beta'
|
||||
|
||||
@@ -5,6 +5,8 @@ import {sha256} from 'js-sha256'
|
||||
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
|
||||
|
||||
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
|
||||
// TODO: Reenable when the build issue is fixed.
|
||||
// import * as bitdrift from '#/lib/bitdrift'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
@@ -16,6 +18,8 @@ import {Gate} from './gates'
|
||||
|
||||
const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV'
|
||||
|
||||
export const initPromise = initialize()
|
||||
|
||||
type StatsigUser = {
|
||||
userID: string | undefined
|
||||
// TODO: Remove when enough users have custom.platform:
|
||||
@@ -95,19 +99,39 @@ export function logEvent<E extends keyof LogEvents>(
|
||||
rawMetadata: LogEvents[E] & FlatJSONRecord,
|
||||
) {
|
||||
try {
|
||||
const fullMetadata = {
|
||||
...rawMetadata,
|
||||
} as Record<string, string> // Statsig typings are unnecessarily strict here.
|
||||
const fullMetadata = toStringRecord(rawMetadata)
|
||||
fullMetadata.routeName = getCurrentRouteName() ?? '(Uninitialized)'
|
||||
if (Statsig.initializeCalled()) {
|
||||
Statsig.logEvent(eventName, null, fullMetadata)
|
||||
}
|
||||
// Intentionally bypass the logger abstraction to log rich objects.
|
||||
console.groupCollapsed(eventName)
|
||||
console.log(fullMetadata)
|
||||
console.groupEnd()
|
||||
// TODO: Reenable when the build issue is fixed.
|
||||
// bitdrift.info(eventName, fullMetadata)
|
||||
} catch (e) {
|
||||
// A log should never interrupt the calling code, whatever happens.
|
||||
logger.error('Failed to log an event', {message: e})
|
||||
}
|
||||
}
|
||||
|
||||
function toStringRecord<E extends keyof LogEvents>(
|
||||
metadata: LogEvents[E] & FlatJSONRecord,
|
||||
): Record<string, string> {
|
||||
const record: Record<string, string> = {}
|
||||
for (let key in metadata) {
|
||||
if (metadata.hasOwnProperty(key)) {
|
||||
if (typeof metadata[key] === 'string') {
|
||||
record[key] = metadata[key]
|
||||
} else {
|
||||
record[key] = JSON.stringify(metadata[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
// We roll our own cache in front of Statsig because it is a singleton
|
||||
// and it's been difficult to get it to behave in a predictable way.
|
||||
// Our own cache ensures consistent evaluation within a single session.
|
||||
@@ -219,9 +243,6 @@ export async function tryFetchGates(
|
||||
// Use this for less common operations where the user would be OK with a delay.
|
||||
timeoutMs = 1500
|
||||
}
|
||||
// Note: This condition is currently false the very first render because
|
||||
// Statsig has not initialized yet. In the future, we can fix this by
|
||||
// doing the initialization ourselves instead of relying on the provider.
|
||||
if (Statsig.initializeCalled()) {
|
||||
await Promise.race([
|
||||
timeout(timeoutMs),
|
||||
|
||||
@@ -5,6 +5,7 @@ import {AppLanguage} from '#/locale/languages'
|
||||
|
||||
test('sanitizeAppLanguageSetting', () => {
|
||||
expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en)
|
||||
expect(sanitizeAppLanguageSetting('el')).toBe(AppLanguage.el)
|
||||
expect(sanitizeAppLanguageSetting('pt-BR')).toBe(AppLanguage.pt_BR)
|
||||
expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi)
|
||||
expect(sanitizeAppLanguageSetting('id')).toBe(AppLanguage.id)
|
||||
@@ -12,4 +13,5 @@ test('sanitizeAppLanguageSetting', () => {
|
||||
expect(sanitizeAppLanguageSetting('en,foo')).toBe(AppLanguage.en)
|
||||
expect(sanitizeAppLanguageSetting('foo,en')).toBe(AppLanguage.en)
|
||||
expect(sanitizeAppLanguageSetting('vi')).toBe(AppLanguage.vi)
|
||||
expect(sanitizeAppLanguageSetting('ne')).toBe(AppLanguage.ne)
|
||||
})
|
||||
|
||||
+12
-10
@@ -41,17 +41,19 @@ export function getLocales() {
|
||||
}
|
||||
|
||||
if (typeof locale.languageTag === 'string') {
|
||||
if (locale.languageTag.startsWith('zh-Hans')) {
|
||||
// Simplified Chinese to zh-CN
|
||||
locale.languageTag = 'zh-CN'
|
||||
if (
|
||||
locale.languageTag.startsWith('zh-Hans') ||
|
||||
locale.languageTag === 'zh-CN'
|
||||
) {
|
||||
// Simplified Chinese to zh-Hans-CN
|
||||
locale.languageTag = 'zh-Hans-CN'
|
||||
}
|
||||
if (locale.languageTag.startsWith('zh-Hant')) {
|
||||
// Traditional Chinese to zh-TW
|
||||
locale.languageTag = 'zh-TW'
|
||||
}
|
||||
if (locale.languageTag.startsWith('yue')) {
|
||||
// Cantonese (Yue) to zh-HK
|
||||
locale.languageTag = 'zh-HK'
|
||||
if (
|
||||
locale.languageTag.startsWith('zh-Hant') ||
|
||||
locale.languageTag === 'zh-TW'
|
||||
) {
|
||||
// Traditional Chinese to zh-Hant-TW
|
||||
locale.languageTag = 'zh-Hant-TW'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+50
-6
@@ -5,6 +5,7 @@ import lande from 'lande'
|
||||
import {hasProp} from '#/lib/type-guards'
|
||||
import {
|
||||
AppLanguage,
|
||||
type Language,
|
||||
LANGUAGES_MAP_CODE2,
|
||||
LANGUAGES_MAP_CODE3,
|
||||
} from './languages'
|
||||
@@ -31,9 +32,44 @@ export function code3ToCode2Strict(lang: string): string | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function codeToLanguageName(lang: string): string {
|
||||
const lang2 = code3ToCode2(lang)
|
||||
return LANGUAGES_MAP_CODE2[lang2]?.name || lang
|
||||
function getLocalizedLanguage(
|
||||
langCode: string,
|
||||
appLang: string,
|
||||
): string | undefined {
|
||||
try {
|
||||
const allNames = new Intl.DisplayNames([appLang], {
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
languageDisplay: 'standard',
|
||||
})
|
||||
const translatedName = allNames.of(langCode)
|
||||
|
||||
if (translatedName) {
|
||||
// force simple title case (as languages do not always start with an uppercase in Unicode data)
|
||||
return translatedName[0].toLocaleUpperCase() + translatedName.slice(1)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore RangeError from Intl.DisplayNames APIs
|
||||
if (!(e instanceof RangeError)) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function languageName(language: Language, appLang: string): string {
|
||||
// if Intl.DisplayNames is unavailable on the target, display the English name
|
||||
if (!(Intl as any).DisplayNames) {
|
||||
return language.name
|
||||
}
|
||||
|
||||
return getLocalizedLanguage(language.code2, appLang) || language.name
|
||||
}
|
||||
|
||||
export function codeToLanguageName(lang2or3: string, appLang: string): string {
|
||||
const code2 = code3ToCode2(lang2or3)
|
||||
const knownLanguage = LANGUAGES_MAP_CODE2[code2]
|
||||
|
||||
return knownLanguage ? languageName(knownLanguage, appLang) : code2
|
||||
}
|
||||
|
||||
export function getPostLanguage(
|
||||
@@ -127,6 +163,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.ca
|
||||
case 'de':
|
||||
return AppLanguage.de
|
||||
case 'el':
|
||||
return AppLanguage.el
|
||||
case 'en-GB':
|
||||
return AppLanguage.en_GB
|
||||
case 'es':
|
||||
@@ -149,14 +187,20 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.it
|
||||
case 'ja':
|
||||
return AppLanguage.ja
|
||||
case 'km':
|
||||
return AppLanguage.km
|
||||
case 'ko':
|
||||
return AppLanguage.ko
|
||||
case 'ne':
|
||||
return AppLanguage.ne
|
||||
case 'nl':
|
||||
return AppLanguage.nl
|
||||
case 'pl':
|
||||
return AppLanguage.pl
|
||||
case 'pt-BR':
|
||||
return AppLanguage.pt_BR
|
||||
case 'ro':
|
||||
return AppLanguage.ro
|
||||
case 'ru':
|
||||
return AppLanguage.ru
|
||||
case 'th':
|
||||
@@ -167,11 +211,11 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.uk
|
||||
case 'vi':
|
||||
return AppLanguage.vi
|
||||
case 'zh-CN':
|
||||
case 'zh-Hans-CN':
|
||||
return AppLanguage.zh_CN
|
||||
case 'zh-HK':
|
||||
case 'zh-Hant-HK':
|
||||
return AppLanguage.zh_HK
|
||||
case 'zh-TW':
|
||||
case 'zh-Hant-TW':
|
||||
return AppLanguage.zh_TW
|
||||
default:
|
||||
continue
|
||||
|
||||
@@ -15,6 +15,7 @@ import {messages as messagesAn} from '#/locale/locales/an/messages'
|
||||
import {messages as messagesAst} from '#/locale/locales/ast/messages'
|
||||
import {messages as messagesCa} from '#/locale/locales/ca/messages'
|
||||
import {messages as messagesDe} from '#/locale/locales/de/messages'
|
||||
import {messages as messagesEl} from '#/locale/locales/el/messages'
|
||||
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
||||
import {messages as messagesEn_GB} from '#/locale/locales/en-GB/messages'
|
||||
import {messages as messagesEs} from '#/locale/locales/es/messages'
|
||||
@@ -27,10 +28,13 @@ import {messages as messagesHu} from '#/locale/locales/hu/messages'
|
||||
import {messages as messagesId} from '#/locale/locales/id/messages'
|
||||
import {messages as messagesIt} from '#/locale/locales/it/messages'
|
||||
import {messages as messagesJa} from '#/locale/locales/ja/messages'
|
||||
import {messages as messagesKm} from '#/locale/locales/km/messages'
|
||||
import {messages as messagesKo} from '#/locale/locales/ko/messages'
|
||||
import {messages as messagesNe} from '#/locale/locales/ne/messages'
|
||||
import {messages as messagesNl} from '#/locale/locales/nl/messages'
|
||||
import {messages as messagesPl} from '#/locale/locales/pl/messages'
|
||||
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
|
||||
import {messages as messagesRo} from '#/locale/locales/ro/messages'
|
||||
import {messages as messagesRu} from '#/locale/locales/ru/messages'
|
||||
import {messages as messagesTh} from '#/locale/locales/th/messages'
|
||||
import {messages as messagesTr} from '#/locale/locales/tr/messages'
|
||||
@@ -78,6 +82,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.el: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEl})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/el'),
|
||||
import('@formatjs/intl-numberformat/locale-data/el'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.en_GB: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEn_GB})
|
||||
await Promise.all([
|
||||
@@ -166,6 +178,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.km: {
|
||||
i18n.loadAndActivate({locale, messages: messagesKm})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/km'),
|
||||
import('@formatjs/intl-numberformat/locale-data/km'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ko: {
|
||||
i18n.loadAndActivate({locale, messages: messagesKo})
|
||||
await Promise.all([
|
||||
@@ -174,6 +194,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ne: {
|
||||
i18n.loadAndActivate({locale, messages: messagesNe})
|
||||
break
|
||||
}
|
||||
case AppLanguage.nl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesNl})
|
||||
await Promise.all([
|
||||
@@ -198,6 +222,14 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ro: {
|
||||
i18n.loadAndActivate({locale, messages: messagesRo})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/ro'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ro'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ru: {
|
||||
i18n.loadAndActivate({locale, messages: messagesRu})
|
||||
await Promise.all([
|
||||
|
||||
@@ -28,6 +28,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/de/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.el: {
|
||||
mod = await import(`./locales/el/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.en_GB: {
|
||||
mod = await import(`./locales/en-GB/messages`)
|
||||
break
|
||||
@@ -72,10 +76,18 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/ja/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.km: {
|
||||
mod = await import(`./locales/km/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ko: {
|
||||
mod = await import(`./locales/ko/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ne: {
|
||||
mod = await import(`./locales/ne/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.nl: {
|
||||
mod = await import(`./locales/nl/messages`)
|
||||
break
|
||||
@@ -88,6 +100,10 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/pt-BR/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ro: {
|
||||
mod = await import(`./locales/ro/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.ru: {
|
||||
mod = await import(`./locales/ru/messages`)
|
||||
break
|
||||
|
||||
+12
-4
@@ -1,4 +1,4 @@
|
||||
interface Language {
|
||||
export interface Language {
|
||||
code3: string
|
||||
code2: string
|
||||
name: string
|
||||
@@ -10,6 +10,7 @@ export enum AppLanguage {
|
||||
ast = 'ast',
|
||||
ca = 'ca',
|
||||
de = 'de',
|
||||
el = 'el',
|
||||
en_GB = 'en-GB',
|
||||
es = 'es',
|
||||
fi = 'fi',
|
||||
@@ -21,18 +22,21 @@ export enum AppLanguage {
|
||||
id = 'id',
|
||||
it = 'it',
|
||||
ja = 'ja',
|
||||
km = 'km',
|
||||
ko = 'ko',
|
||||
ne = 'ne',
|
||||
nl = 'nl',
|
||||
pl = 'pl',
|
||||
pt_BR = 'pt-BR',
|
||||
ro = 'ro',
|
||||
ru = 'ru',
|
||||
th = 'th',
|
||||
tr = 'tr',
|
||||
uk = 'uk',
|
||||
vi = 'vi',
|
||||
zh_CN = 'zh-CN',
|
||||
zh_HK = 'zh-HK',
|
||||
zh_TW = 'zh-TW',
|
||||
zh_CN = 'zh-Hans-CN',
|
||||
zh_HK = 'zh-Hant-HK',
|
||||
zh_TW = 'zh-Hant-TW',
|
||||
}
|
||||
|
||||
interface AppLanguageConfig {
|
||||
@@ -46,6 +50,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
|
||||
{code2: AppLanguage.ast, name: 'Asturianu – Asturian'},
|
||||
{code2: AppLanguage.ca, name: 'Català – Catalan'},
|
||||
{code2: AppLanguage.de, name: 'Deutsch – German'},
|
||||
{code2: AppLanguage.el, name: 'Ελληνικά – Greek'},
|
||||
{code2: AppLanguage.en_GB, name: 'English (UK)'},
|
||||
{code2: AppLanguage.es, name: 'Español – Spanish'},
|
||||
{code2: AppLanguage.fi, name: 'Suomi – Finnish'},
|
||||
@@ -57,10 +62,13 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
|
||||
{code2: AppLanguage.id, name: 'Bahasa Indonesia – Indonesian'},
|
||||
{code2: AppLanguage.it, name: 'Italiano – Italian'},
|
||||
{code2: AppLanguage.ja, name: '日本語 – Japanese'},
|
||||
{code2: AppLanguage.km, name: 'ភាសាខ្មែរ – Khmer'},
|
||||
{code2: AppLanguage.ko, name: '한국어 – Korean'},
|
||||
{code2: AppLanguage.ne, name: 'नेपाली – Nepali'},
|
||||
{code2: AppLanguage.nl, name: 'Nederlands – Dutch'},
|
||||
{code2: AppLanguage.pl, name: 'Polski – Polish'},
|
||||
{code2: AppLanguage.pt_BR, name: 'Português (BR) – Portuguese (BR)'},
|
||||
{code2: AppLanguage.ro, name: 'Română – Romanian'},
|
||||
{code2: AppLanguage.ru, name: 'Русский – Russian'},
|
||||
{code2: AppLanguage.th, name: 'ภาษาไทย – Thai'},
|
||||
{code2: AppLanguage.tr, name: 'Türkçe – Turkish'},
|
||||
|
||||
+1632
-2109
File diff suppressed because it is too large
Load Diff
+1667
-1271
File diff suppressed because it is too large
Load Diff
+1500
-1112
File diff suppressed because it is too large
Load Diff
+1495
-1107
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1491
-1095
File diff suppressed because it is too large
Load Diff
+1491
-1095
File diff suppressed because it is too large
Load Diff
+1651
-1263
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user