Compare commits
103 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 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+8
-2
@@ -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,12 +225,14 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
'react-native-compressor',
|
||||
'@bitdrift/react-native',
|
||||
// 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',
|
||||
@@ -341,6 +346,7 @@ module.exports = function (config) {
|
||||
},
|
||||
},
|
||||
],
|
||||
['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
|
||||
+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>
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
|
||||
+29
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.96.0",
|
||||
"version": "1.96.4",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -54,8 +54,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.13.20",
|
||||
"@bitdrift/react-native": "0.4.0",
|
||||
"@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",
|
||||
@@ -63,7 +62,6 @@
|
||||
"@expo/webpack-config": "^19.0.0",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@floating-ui/react-dom": "^2.0.8",
|
||||
"@formatjs/intl-datetimeformat": "^6.17.1",
|
||||
"@formatjs/intl-locale": "^4.2.8",
|
||||
"@formatjs/intl-numberformat": "^8.15.1",
|
||||
"@formatjs/intl-pluralrules": "^5.4.1",
|
||||
@@ -71,7 +69,7 @@
|
||||
"@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",
|
||||
@@ -140,6 +138,7 @@
|
||||
"expo-media-library": "~17.0.3",
|
||||
"expo-navigation-bar": "~4.0.4",
|
||||
"expo-notifications": "~0.29.11",
|
||||
"expo-screen-orientation": "^8.0.2",
|
||||
"expo-sharing": "^13.0.0",
|
||||
"expo-splash-screen": "~0.29.18",
|
||||
"expo-status-bar": "~2.0.0",
|
||||
@@ -172,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",
|
||||
@@ -251,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",
|
||||
@@ -330,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,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 ->
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
+15
-7
@@ -10,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'
|
||||
@@ -22,7 +23,7 @@ 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'
|
||||
@@ -57,6 +58,7 @@ 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'
|
||||
@@ -76,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
|
||||
@@ -143,12 +149,14 @@ function InnerApp() {
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</GestureHandlerRootView>
|
||||
<TrendingConfigProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</GestureHandlerRootView>
|
||||
</TrendingConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
|
||||
+5
-2
@@ -47,6 +47,7 @@ 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'
|
||||
@@ -127,8 +128,10 @@ function InnerApp() {
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
<TrendingConfigProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</TrendingConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,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} />
|
||||
|
||||
@@ -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]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,12 +45,14 @@ 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_xs, {minHeight: 48}],
|
||||
@@ -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>) {
|
||||
|
||||
@@ -4,7 +4,7 @@ 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'
|
||||
@@ -122,9 +122,21 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onPress={async e => {
|
||||
context.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 => {
|
||||
onPressIn()
|
||||
@@ -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 (
|
||||
|
||||
@@ -304,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])
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,5 +12,5 @@ export function useHeaderOffset() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
+24
-17
@@ -1,20 +1,27 @@
|
||||
import {init} from '@bitdrift/react-native'
|
||||
import {Statsig} from 'statsig-react-native-expo'
|
||||
// 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'
|
||||
// import {initPromise} from './statsig/statsig'
|
||||
|
||||
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
|
||||
// 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'})
|
||||
}
|
||||
})
|
||||
// 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() {}
|
||||
@@ -21,7 +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 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({
|
||||
|
||||
@@ -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
|
||||
@@ -92,6 +93,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
|
||||
Feeds: undefined
|
||||
Notifications: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Topic: {topic: string}
|
||||
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||
}
|
||||
|
||||
@@ -105,6 +107,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
||||
Notifications: undefined
|
||||
MyProfileTab: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Topic: {topic: string}
|
||||
MessagesTab: undefined
|
||||
Messages: {animation?: 'push' | 'pop'}
|
||||
Start: {name: string; rkey: string}
|
||||
|
||||
@@ -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,3 +1,7 @@
|
||||
export type Gate =
|
||||
// Keep this alphabetic please.
|
||||
'debug_show_feedcontext' | 'debug_subscriptions' | 'remove_show_latest_button'
|
||||
| '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'
|
||||
@@ -97,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+32
-29
@@ -1,10 +1,8 @@
|
||||
// Don't remove -force from these because detection is VERY slow on low-end Android.
|
||||
// https://github.com/formatjs/formatjs/issues/4463#issuecomment-2176070577
|
||||
import '@formatjs/intl-locale/polyfill-force'
|
||||
import '@formatjs/intl-datetimeformat/polyfill-force'
|
||||
import '@formatjs/intl-pluralrules/polyfill-force'
|
||||
import '@formatjs/intl-numberformat/polyfill-force'
|
||||
import '@formatjs/intl-datetimeformat/locale-data/en'
|
||||
import '@formatjs/intl-pluralrules/locale-data/en'
|
||||
import '@formatjs/intl-numberformat/locale-data/en'
|
||||
|
||||
@@ -17,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'
|
||||
@@ -29,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'
|
||||
@@ -51,7 +53,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.an: {
|
||||
i18n.loadAndActivate({locale, messages: messagesAn})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/es'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/an'),
|
||||
import('@formatjs/intl-numberformat/locale-data/es'),
|
||||
])
|
||||
@@ -60,7 +61,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.ast: {
|
||||
i18n.loadAndActivate({locale, messages: messagesAst})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ast'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ast'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ast'),
|
||||
])
|
||||
@@ -69,7 +69,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.ca: {
|
||||
i18n.loadAndActivate({locale, messages: messagesCa})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ca'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ca'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ca'),
|
||||
])
|
||||
@@ -78,16 +77,22 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.de: {
|
||||
i18n.loadAndActivate({locale, messages: messagesDe})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/de'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/de'),
|
||||
import('@formatjs/intl-numberformat/locale-data/de'),
|
||||
])
|
||||
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([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/en-GB'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/en'),
|
||||
import('@formatjs/intl-numberformat/locale-data/en-GB'),
|
||||
])
|
||||
@@ -96,7 +101,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.es: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEs})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/es'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/es'),
|
||||
import('@formatjs/intl-numberformat/locale-data/es'),
|
||||
])
|
||||
@@ -105,7 +109,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.fi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFi})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/fi'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/fi'),
|
||||
import('@formatjs/intl-numberformat/locale-data/fi'),
|
||||
])
|
||||
@@ -114,7 +117,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.fr: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFr})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/fr'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/fr'),
|
||||
import('@formatjs/intl-numberformat/locale-data/fr'),
|
||||
])
|
||||
@@ -123,7 +125,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.ga: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGa})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ga'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ga'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ga'),
|
||||
])
|
||||
@@ -132,7 +133,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.gl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGl})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/gl'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/gl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/gl'),
|
||||
])
|
||||
@@ -141,7 +141,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.hi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesHi})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/hi'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/hi'),
|
||||
import('@formatjs/intl-numberformat/locale-data/hi'),
|
||||
])
|
||||
@@ -150,7 +149,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.hu: {
|
||||
i18n.loadAndActivate({locale, messages: messagesHu})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/hu'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/hu'),
|
||||
import('@formatjs/intl-numberformat/locale-data/hu'),
|
||||
])
|
||||
@@ -159,7 +157,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.id: {
|
||||
i18n.loadAndActivate({locale, messages: messagesId})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/id'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/id'),
|
||||
import('@formatjs/intl-numberformat/locale-data/id'),
|
||||
])
|
||||
@@ -168,7 +165,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.it: {
|
||||
i18n.loadAndActivate({locale, messages: messagesIt})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/it'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/it'),
|
||||
import('@formatjs/intl-numberformat/locale-data/it'),
|
||||
])
|
||||
@@ -177,25 +173,34 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.ja: {
|
||||
i18n.loadAndActivate({locale, messages: messagesJa})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ja'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ja'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ja'),
|
||||
])
|
||||
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([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ko'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ko'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ko'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.ne: {
|
||||
i18n.loadAndActivate({locale, messages: messagesNe})
|
||||
break
|
||||
}
|
||||
case AppLanguage.nl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesNl})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/nl'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/nl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/nl'),
|
||||
])
|
||||
@@ -204,7 +209,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.pl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesPl})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/pl'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/pl'),
|
||||
import('@formatjs/intl-numberformat/locale-data/pl'),
|
||||
])
|
||||
@@ -213,16 +217,22 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.pt_BR: {
|
||||
i18n.loadAndActivate({locale, messages: messagesPt_BR})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/pt'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/pt'),
|
||||
import('@formatjs/intl-numberformat/locale-data/pt'),
|
||||
])
|
||||
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([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/ru'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/ru'),
|
||||
import('@formatjs/intl-numberformat/locale-data/ru'),
|
||||
])
|
||||
@@ -231,7 +241,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.th: {
|
||||
i18n.loadAndActivate({locale, messages: messagesTh})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/th'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/th'),
|
||||
import('@formatjs/intl-numberformat/locale-data/th'),
|
||||
])
|
||||
@@ -240,7 +249,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.tr: {
|
||||
i18n.loadAndActivate({locale, messages: messagesTr})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/tr'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/tr'),
|
||||
import('@formatjs/intl-numberformat/locale-data/tr'),
|
||||
])
|
||||
@@ -249,7 +257,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.uk: {
|
||||
i18n.loadAndActivate({locale, messages: messagesUk})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/uk'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/uk'),
|
||||
import('@formatjs/intl-numberformat/locale-data/uk'),
|
||||
])
|
||||
@@ -258,7 +265,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.vi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesVi})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/vi'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/vi'),
|
||||
import('@formatjs/intl-numberformat/locale-data/vi'),
|
||||
])
|
||||
@@ -267,7 +273,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.zh_CN: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_CN})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/zh-Hans'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh'),
|
||||
])
|
||||
@@ -276,7 +281,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.zh_HK: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_HK})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/yue'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh'),
|
||||
])
|
||||
@@ -285,7 +289,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
case AppLanguage.zh_TW: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_TW})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-datetimeformat/locale-data/zh-Hant'),
|
||||
import('@formatjs/intl-pluralrules/locale-data/zh'),
|
||||
import('@formatjs/intl-numberformat/locale-data/zh'),
|
||||
])
|
||||
|
||||
@@ -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'},
|
||||
|
||||
+1631
-2108
File diff suppressed because it is too large
Load Diff
+1666
-1270
File diff suppressed because it is too large
Load Diff
+1499
-1111
File diff suppressed because it is too large
Load Diff
+1494
-1106
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
+1650
-1262
File diff suppressed because it is too large
Load Diff
+2618
-4001
File diff suppressed because it is too large
Load Diff
+1432
-1707
File diff suppressed because it is too large
Load Diff
+1498
-1102
File diff suppressed because it is too large
Load Diff
+1495
-1107
File diff suppressed because it is too large
Load Diff
+1498
-1102
File diff suppressed because it is too large
Load Diff
+1531
-1177
File diff suppressed because it is too large
Load Diff
+1494
-1106
File diff suppressed because it is too large
Load Diff
+1729
-1491
File diff suppressed because it is too large
Load Diff
+1434
-1233
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1433
-1204
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1640
-1746
File diff suppressed because it is too large
Load Diff
+2468
-2072
File diff suppressed because it is too large
Load Diff
+1517
-1129
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1947
-2423
File diff suppressed because it is too large
Load Diff
+1493
-1105
File diff suppressed because it is too large
Load Diff
+1928
-1540
File diff suppressed because it is too large
Load Diff
+1491
-1103
File diff suppressed because it is too large
Load Diff
+1683
-1835
File diff suppressed because it is too large
Load Diff
+1572
-1323
File diff suppressed because it is too large
Load Diff
+2124
-1873
File diff suppressed because it is too large
Load Diff
+1648
-1399
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,7 @@ import {
|
||||
error as bdError,
|
||||
info as bdInfo,
|
||||
warn as bdWarn,
|
||||
} from '@bitdrift/react-native'
|
||||
|
||||
} from '../lib/bitdrift'
|
||||
import {LogLevel, Transport} from './types'
|
||||
|
||||
export function createBitdriftTransport(): Transport {
|
||||
@@ -18,6 +17,6 @@ export function createBitdriftTransport(): Transport {
|
||||
|
||||
return (level, message) => {
|
||||
const log = logFunctions[level]
|
||||
log(message.toString())
|
||||
log('' + message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import {Transport} from './index'
|
||||
|
||||
export function createBitdriftTransport(): Transport {
|
||||
return (_level, _message) => {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ export const router = new Router({
|
||||
CopyrightPolicy: '/support/copyright',
|
||||
// hashtags
|
||||
Hashtag: '/hashtag/:tag',
|
||||
Topic: '/topic/:topic',
|
||||
// DMs
|
||||
Messages: '/messages',
|
||||
MessagesSettings: '/messages/settings',
|
||||
|
||||
+24
-24
@@ -107,34 +107,34 @@ export default function HashtagScreen({
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer noBottomBorder>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText>
|
||||
{author && (
|
||||
<Layout.Header.SubtitleText>
|
||||
{_(msg`From @${sanitizedAuthor}`)}
|
||||
</Layout.Header.SubtitleText>
|
||||
)}
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot>
|
||||
<Button
|
||||
label={_(msg`Share`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
shape="round"
|
||||
onPress={onShare}
|
||||
hitSlop={HITSLOP_10}
|
||||
style={[{right: -3}]}>
|
||||
<ButtonIcon icon={Share} size="md" />
|
||||
</Button>
|
||||
</Layout.Header.Slot>
|
||||
</Layout.Header.Outer>
|
||||
<Pager
|
||||
onPageSelected={onPageSelected}
|
||||
renderTabBar={props => (
|
||||
<Layout.Center style={[a.z_10, web([a.sticky, {top: 0}])]}>
|
||||
<Layout.Header.Outer noBottomBorder>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText>
|
||||
{author && (
|
||||
<Layout.Header.SubtitleText>
|
||||
{_(msg`From @${sanitizedAuthor}`)}
|
||||
</Layout.Header.SubtitleText>
|
||||
)}
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot>
|
||||
<Button
|
||||
label={_(msg`Share`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
shape="round"
|
||||
onPress={onShare}
|
||||
hitSlop={HITSLOP_10}
|
||||
style={[{right: -3}]}>
|
||||
<ButtonIcon icon={Share} size="md" />
|
||||
</Button>
|
||||
</Layout.Header.Slot>
|
||||
</Layout.Header.Outer>
|
||||
<TabBar items={sections.map(section => section.title)} {...props} />
|
||||
</Layout.Center>
|
||||
)}
|
||||
|
||||
@@ -123,7 +123,11 @@ export function ListHiddenScreen({
|
||||
/>
|
||||
<View style={[a.gap_sm, a.align_center]}>
|
||||
<Text style={[a.font_bold, a.text_3xl]}>
|
||||
<Trans>List has been hidden</Trans>
|
||||
{list.creator.viewer?.blocking || list.creator.viewer?.blockedBy ? (
|
||||
<Trans>Creator has been blocked</Trans>
|
||||
) : (
|
||||
<Trans>List has been hidden</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
@@ -133,16 +137,23 @@ export function ListHiddenScreen({
|
||||
t.atoms.text_contrast_high,
|
||||
{lineHeight: 1.4},
|
||||
]}>
|
||||
<Trans>
|
||||
This list - created by{' '}
|
||||
<Text style={[a.text_md, !isOwner && a.font_bold]}>
|
||||
{isOwner
|
||||
? _(msg`you`)
|
||||
: sanitizeHandle(list.creator.handle, '@')}
|
||||
</Text>{' '}
|
||||
- contains possible violations of Bluesky's community guidelines
|
||||
in its name or description.
|
||||
</Trans>
|
||||
{list.creator.viewer?.blocking || list.creator.viewer?.blockedBy ? (
|
||||
<Trans>
|
||||
Either the creator of this list has blocked you or you have
|
||||
blocked the creator.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
This list - created by{' '}
|
||||
<Text style={[a.text_md, !isOwner && a.font_bold]}>
|
||||
{isOwner
|
||||
? _(msg`you`)
|
||||
: sanitizeHandle(list.creator.handle, '@')}
|
||||
</Text>{' '}
|
||||
- contains possible violations of Bluesky's community guidelines
|
||||
in its name or description.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -165,7 +165,9 @@ export function MessagesScreen({navigation, route}: Props) {
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
onPress={() => refetch()}>
|
||||
<ButtonText>Retry</ButtonText>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={Retry} position="right" />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Pressable, StyleSheet, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import Graphemer from 'graphemer'
|
||||
import {flushSync} from 'react-dom'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
|
||||
import {isSafari, isTouchDevice} from '#/lib/browser'
|
||||
@@ -106,11 +107,19 @@ export function MessageInput({
|
||||
|
||||
const onEmojiInserted = React.useCallback(
|
||||
(emoji: Emoji) => {
|
||||
const position = textAreaRef.current?.selectionStart ?? 0
|
||||
setMessage(
|
||||
message =>
|
||||
message.slice(0, position) + emoji.native + message.slice(position),
|
||||
)
|
||||
if (!textAreaRef.current) {
|
||||
return
|
||||
}
|
||||
const position = textAreaRef.current.selectionStart ?? 0
|
||||
textAreaRef.current.focus()
|
||||
flushSync(() => {
|
||||
setMessage(
|
||||
message =>
|
||||
message.slice(0, position) + emoji.native + message.slice(position),
|
||||
)
|
||||
})
|
||||
textAreaRef.current.selectionStart = position + emoji.native.length
|
||||
textAreaRef.current.selectionEnd = position + emoji.native.length
|
||||
},
|
||||
[setMessage],
|
||||
)
|
||||
@@ -148,7 +157,14 @@ export function MessageInput({
|
||||
<Button
|
||||
onPress={e => {
|
||||
e.currentTarget.measure((_fx, _fy, _width, _height, px, py) => {
|
||||
openEmojiPicker?.({top: py, left: px, right: px, bottom: py})
|
||||
openEmojiPicker?.({
|
||||
top: py,
|
||||
left: px,
|
||||
right: px,
|
||||
bottom: py,
|
||||
nextFocusRef:
|
||||
textAreaRef as unknown as React.MutableRefObject<HTMLElement>,
|
||||
})
|
||||
})
|
||||
}}
|
||||
style={[
|
||||
|
||||
@@ -101,7 +101,7 @@ export function MessagesList({
|
||||
const [emojiPickerState, setEmojiPickerState] =
|
||||
React.useState<EmojiPickerState>({
|
||||
isOpen: false,
|
||||
pos: {top: 0, left: 0, right: 0, bottom: 0},
|
||||
pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null},
|
||||
})
|
||||
|
||||
// We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
TIMELINE_SAVED_FEED,
|
||||
} from '#/lib/constants'
|
||||
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
|
||||
import {getAllListMembers} from '#/state/queries/list-members'
|
||||
@@ -57,6 +57,7 @@ export function StepFinished() {
|
||||
const setActiveStarterPack = useSetActiveStarterPack()
|
||||
const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
|
||||
const {startProgressGuide} = useProgressGuideControls()
|
||||
const gate = useGate()
|
||||
|
||||
const finishOnboarding = React.useCallback(async () => {
|
||||
setSaving(true)
|
||||
@@ -190,7 +191,9 @@ export function StepFinished() {
|
||||
setSaving(false)
|
||||
setActiveStarterPack(undefined)
|
||||
setHasCheckedForStarterPack(true)
|
||||
startProgressGuide('like-10-and-follow-7')
|
||||
startProgressGuide(
|
||||
gate('new_postonboarding') ? 'follow-10' : 'like-10-and-follow-7',
|
||||
)
|
||||
dispatch({type: 'finish'})
|
||||
onboardDispatch({type: 'finish'})
|
||||
logEvent('onboarding:finished:nextPressed', {
|
||||
@@ -221,6 +224,7 @@ export function StepFinished() {
|
||||
setActiveStarterPack,
|
||||
setHasCheckedForStarterPack,
|
||||
startProgressGuide,
|
||||
gate,
|
||||
])
|
||||
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user