Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df4c03ee07 | |||
| ea00663e7f | |||
| 12e57378e8 | |||
| 24606dbcfc | |||
| 6f31121e51 | |||
| 1abc96314b | |||
| 7cc897d096 | |||
| 6ed30213c8 | |||
| d7e8429753 | |||
| 6086bc5d5c | |||
| 199821d670 | |||
| 11dff765db | |||
| 73639aa571 | |||
| f3b6d85913 | |||
| 239d3cc535 | |||
| d49d43f6eb | |||
| a482dae936 | |||
| 25d44d8939 | |||
| e7801a0b76 | |||
| 7861cd153d | |||
| be53a168ba | |||
| b56d0c419b | |||
| 03adb4260f | |||
| 27926f093c | |||
| fe5a623b44 | |||
| fc944841ce | |||
| 66ca3e34a4 | |||
| 4788106f32 | |||
| 6414bda252 | |||
| 591fd262b3 | |||
| ede10368d5 | |||
| 1127e50248 | |||
| e4485da4ff | |||
| 6c7fd96528 | |||
| 6a1c0ff346 | |||
| ad18439148 | |||
| 7280e44ea2 | |||
| 7e6b23bd86 | |||
| d2bc22e9be | |||
| 0b50e9f109 | |||
| 0f6a99c8a5 | |||
| ea9986f127 | |||
| 5222a6e1c3 | |||
| b7f505ab00 | |||
| 34f2cc2322 | |||
| 8a30131686 |
@@ -50,8 +50,8 @@ jobs:
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
id: timestamp
|
||||
uses: nanzm/get-time-action@master
|
||||
with:
|
||||
format: 'MM-DD-HH-mm-ss'
|
||||
format: "MM-DD-HH-mm-ss"
|
||||
|
||||
- name: 🚀 Upload Production Artifact
|
||||
id: upload-artifact-production
|
||||
@@ -146,6 +146,11 @@ jobs:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: 🧹 Clear Metro cache
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
# https://github.com/expo/eas-cli/issues/2959#issuecomment-2749791326
|
||||
run: rm -rf ${TMPDIR:-/tmp}/metro-cache
|
||||
|
||||
- name: 🏗️ Build Production APK
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
run: >
|
||||
@@ -182,11 +187,11 @@ jobs:
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
if: ${{ inputs.profile == 'testflight-android' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
if: ${{ inputs.profile == 'testflight-android' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
build:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "16.4"
|
||||
xcode-version: "26.0"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
@@ -98,10 +98,29 @@ jobs:
|
||||
yarn use-build-number-with-bump
|
||||
eas build -p ios
|
||||
--profile ${{ inputs.profile || 'testflight' }}
|
||||
--local --output build.ipa --non-interactive
|
||||
--local --output build.tar.gz --non-interactive
|
||||
|
||||
- name: 📂 Extract build artifact
|
||||
run: |
|
||||
if [ -f "build.tar.gz" ]; then
|
||||
echo "Extracting build.tar.gz..."
|
||||
mkdir ios-build
|
||||
tar -xzf build.tar.gz -C ios-build
|
||||
echo "Extraction completed successfully"
|
||||
else
|
||||
echo "Archive file not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
run: eas submit -p ios --non-interactive --path ios-build/ios/build/Bluesky.ipa
|
||||
|
||||
- name: 🪲 Upload dSYM to Sentry
|
||||
run: >
|
||||
SENTRY_ORG=blueskyweb
|
||||
SENTRY_PROJECT=app
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
yarn sentry-cli debug-files upload ios-build/ios/build/Bluesky.app.dSYM.zip --include-sources
|
||||
|
||||
- name: 📚 Get version from package.json
|
||||
id: get-build-info
|
||||
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
# GitHub actions are horrible so let's just copy paste this in
|
||||
buildIfNecessaryIOS:
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
||||
cancel-in-progress: true
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "16.2"
|
||||
xcode-version: "26.0"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
|
||||
@@ -3,7 +3,7 @@ appId: xyz.blueskyweb.app
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ""
|
||||
SERVER_PATH: ""
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
@@ -25,7 +25,10 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn:
|
||||
id: "passwordInput"
|
||||
- inputText: "hunter22"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
# hideKeyboard on ios is borked
|
||||
# https://docs.maestro.dev/troubleshooting/known-issues#hidekeyboard-command-is-flaky
|
||||
text: "Your account"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn:
|
||||
@@ -34,7 +37,7 @@ appId: xyz.blueskyweb.app
|
||||
- inputText: "e2e-test"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "handleAvailableCheck"
|
||||
id: "handleAvailableCheck"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- assertVisible: "Give your profile a face"
|
||||
|
||||
@@ -17,63 +17,52 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eGotoLists"
|
||||
- tapOn:
|
||||
id: "newUserListBtn"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- waitForAnimationToEnd
|
||||
- assertVisible: "Create user list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- inputText: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Create user list"
|
||||
- tapOn: "People"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
|
||||
- tapOn:
|
||||
label: "Edit display name and description via the edit curatelist modal"
|
||||
point: "90%,9%"
|
||||
id: "moreOptionsBtn"
|
||||
- tapOn: "Edit list details"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: "Edit user list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- eraseText
|
||||
- inputText: "Bad Ppl"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- eraseText
|
||||
- inputText: "They bad"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Edit user list"
|
||||
- assertVisible: Bad Ppl
|
||||
- assertVisible: They bad
|
||||
|
||||
- tapOn:
|
||||
label: "Remove description via the edit curatelist modal"
|
||||
point: "90%,9%"
|
||||
id: "moreOptionsBtn"
|
||||
- tapOn: "Edit list details"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: "Edit user list"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- eraseText
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Edit user list"
|
||||
- assertNotVisible:
|
||||
id: "listDescription"
|
||||
|
||||
- tapOn:
|
||||
label: "Delete the curatelist"
|
||||
point: "90%,9%"
|
||||
id: "moreOptionsBtn"
|
||||
- tapOn: "Delete List"
|
||||
- tapOn:
|
||||
id: "confirmBtn"
|
||||
@@ -82,18 +71,15 @@ appId: xyz.blueskyweb.app
|
||||
id: "newUserListBtn"
|
||||
- tapOn:
|
||||
id: "newUserListBtn"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: "Create user list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- inputText: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible: "Create user list"
|
||||
- tapOn: "People"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
|
||||
@@ -19,21 +19,17 @@ appId: xyz.blueskyweb.app
|
||||
id: "moderationlistsBtn"
|
||||
- tapOn: "New list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
id: "editListNameInput"
|
||||
- inputText: "Muted Users"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
id: "editListDescriptionInput"
|
||||
- inputText: "Shhh"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
|
||||
# view modlist
|
||||
- assertVisible: "Muted Users"
|
||||
- assertVisible: "Shhh"
|
||||
|
||||
# DOES NOT WORK - THE BUTTON IS NOT ACCESSIBLE
|
||||
# IGNORING FOR NOW, FIX THE COMPONENTS IN THE NEXT RELEASE
|
||||
# BECAUSE THIS IS A LEGIT A11Y PROBLEM -sfn
|
||||
- tapOn: "Subscribe to this list"
|
||||
- tapOn: "Mute accounts"
|
||||
- tapOn: "Mute list"
|
||||
|
||||
@@ -30,7 +30,14 @@ appId: xyz.blueskyweb.app
|
||||
- assertVisible: "What are your interests?"
|
||||
- tapOn:
|
||||
id: "onboardingContinue"
|
||||
- assertVisible: "You're ready to go!"
|
||||
- assertVisible: "Suggested for you"
|
||||
# mock server doesn't have suggestions api
|
||||
- tapOn: "Skip to next step"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- assertVisible: "Complete onboarding and start using your account"
|
||||
- tapOn:
|
||||
id: "onboardingFinish"
|
||||
- tapOn:
|
||||
|
||||
@@ -16,7 +16,8 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eStartOnboarding"
|
||||
- tapOn: "Select an avatar"
|
||||
- waitForAnimationToEnd
|
||||
- assertVisible: "Search your library…"
|
||||
- assertVisible: "Photos"
|
||||
- assertVisible: "Collections"
|
||||
- tapOn:
|
||||
point: "50%,22%"
|
||||
- waitForAnimationToEnd
|
||||
@@ -27,7 +28,14 @@ appId: xyz.blueskyweb.app
|
||||
- assertVisible: "What are your interests?"
|
||||
- tapOn:
|
||||
id: "onboardingContinue"
|
||||
- assertVisible: "You're ready to go!"
|
||||
- assertVisible: "Suggested for you"
|
||||
# mock server doesn't have suggestions api
|
||||
- tapOn: "Skip to next step"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- assertVisible: "Complete onboarding and start using your account"
|
||||
- tapOn:
|
||||
id: "onboardingFinish"
|
||||
- tapOn:
|
||||
|
||||
@@ -31,6 +31,8 @@ appId: xyz.blueskyweb.app
|
||||
id: "viewHeaderHomeFeedPrefsBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 1"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
@@ -69,6 +71,8 @@ appId: xyz.blueskyweb.app
|
||||
id: "profilePager-selector-1"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 2"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- launchApp:
|
||||
appId: "xyz.blueskyweb.app"
|
||||
clearState: true
|
||||
appId: "xyz.blueskyweb.app"
|
||||
clearState: true
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "http://localhost:8081"
|
||||
- waitForAnimationToEnd
|
||||
- extendedWaitUntil:
|
||||
visible: "Continue"
|
||||
- swipe:
|
||||
from: "Bluesky"
|
||||
direction: DOWN
|
||||
from: "Bluesky"
|
||||
direction: DOWN
|
||||
duration: 100
|
||||
- tapOn:
|
||||
id: e2eProxyHeaderInput
|
||||
id: e2eProxyHeaderInput
|
||||
- inputText: ${output.result}
|
||||
- pressKey: Enter
|
||||
- hideKeyboard
|
||||
|
||||
+32
-4
@@ -40,7 +40,7 @@ module.exports = function (_config) {
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
icon: './assets/app-icons/ios_icon_default_light.png',
|
||||
icon: './assets/app-icons/ios_icon_default_next.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
primaryColor: '#1083fe',
|
||||
newArchEnabled: false,
|
||||
@@ -112,6 +112,34 @@ module.exports = function (_config) {
|
||||
'com.apple.security.application-groups': 'group.app.bsky',
|
||||
},
|
||||
privacyManifests: {
|
||||
NSPrivacyCollectedDataTypes: [
|
||||
{
|
||||
NSPrivacyCollectedDataType: 'NSPrivacyCollectedDataTypeCrashData',
|
||||
NSPrivacyCollectedDataTypeLinked: false,
|
||||
NSPrivacyCollectedDataTypeTracking: false,
|
||||
NSPrivacyCollectedDataTypePurposes: [
|
||||
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
|
||||
],
|
||||
},
|
||||
{
|
||||
NSPrivacyCollectedDataType:
|
||||
'NSPrivacyCollectedDataTypePerformanceData',
|
||||
NSPrivacyCollectedDataTypeLinked: false,
|
||||
NSPrivacyCollectedDataTypeTracking: false,
|
||||
NSPrivacyCollectedDataTypePurposes: [
|
||||
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
|
||||
],
|
||||
},
|
||||
{
|
||||
NSPrivacyCollectedDataType:
|
||||
'NSPrivacyCollectedDataTypeOtherDiagnosticData',
|
||||
NSPrivacyCollectedDataTypeLinked: false,
|
||||
NSPrivacyCollectedDataTypeTracking: false,
|
||||
NSPrivacyCollectedDataTypePurposes: [
|
||||
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
|
||||
],
|
||||
},
|
||||
],
|
||||
NSPrivacyAccessedAPITypes: [
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
@@ -143,7 +171,7 @@ module.exports = function (_config) {
|
||||
barStyle: 'light-content',
|
||||
},
|
||||
android: {
|
||||
icon: './assets/app-icons/android_icon_default_light.png',
|
||||
icon: './assets/app-icons/android_icon_default_next.png',
|
||||
adaptiveIcon: {
|
||||
foregroundImage: './assets/icon-android-foreground.png',
|
||||
monochromeImage: './assets/icon-android-foreground.png',
|
||||
@@ -305,8 +333,8 @@ module.exports = function (_config) {
|
||||
prerendered: true,
|
||||
},
|
||||
next: {
|
||||
ios: './assets/app-icons/icon_default_next.png',
|
||||
android: './assets/app-icons/icon_default_next.png',
|
||||
ios: './assets/app-icons/ios_icon_default_next.png',
|
||||
android: './assets/app-icons/android_icon_default_next.png',
|
||||
prerendered: true,
|
||||
},
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 369 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 486 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
Vendored
+1
-1
@@ -15,7 +15,7 @@ import {redirectLogger} from '../logger.js'
|
||||
|
||||
const SAFELINK_MIN_FETCH_INTERVAL = 1_000
|
||||
const SAFELINK_MAX_FETCH_INTERVAL = 10_000
|
||||
const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+.-]*:/
|
||||
const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//
|
||||
|
||||
export class SafelinkClient {
|
||||
private domainCache: LRUCache<string, SafelinkRule | 'ok'>
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ After you do `yarn ios` and `yarn android` once, you can later just run `yarn we
|
||||
|
||||
- Start in various console tabs:
|
||||
- `yarn e2e:mock-server`
|
||||
- `yarn e2e:metro`
|
||||
- `yarn e2e:start`
|
||||
- Run once: `yarn e2e:build`
|
||||
- Each test run: `yarn e2e:run`
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ values.
|
||||
### Running Maestro tests
|
||||
|
||||
- In one tab, run `yarn e2e:mock-server`
|
||||
- In a second tab, run `yarn e2e:metro`
|
||||
- In a second tab, run `yarn e2e:build`
|
||||
- In a third tab, run `yarn e2e:run`
|
||||
|
||||
## Using Flashlight for Performance Testing
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"extends": "base",
|
||||
"ios": {
|
||||
"resourceClass": "large",
|
||||
"autoIncrement": true
|
||||
"autoIncrement": true,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
@@ -50,7 +51,8 @@
|
||||
"extends": "base",
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"autoIncrement": false
|
||||
"autoIncrement": false,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": false
|
||||
@@ -63,7 +65,8 @@
|
||||
"testflight": {
|
||||
"extends": "base",
|
||||
"ios": {
|
||||
"autoIncrement": true
|
||||
"autoIncrement": true,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
@@ -77,7 +80,8 @@
|
||||
"extends": "base",
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"autoIncrement": true
|
||||
"autoIncrement": true,
|
||||
"buildArtifactPaths": ["ios/build/*"]
|
||||
},
|
||||
"android": {
|
||||
"autoIncrement": true
|
||||
|
||||
+6
-4
@@ -46,8 +46,9 @@
|
||||
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
|
||||
"typecheck": "tsc --project ./tsconfig.check.json",
|
||||
"e2e:mock-server": "NODE_ENV=development ./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
|
||||
"e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
||||
"e2e:metro-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
|
||||
"e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
||||
"e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
|
||||
"e2e:start": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo start",
|
||||
"e2e:run": "maestro test __e2e__",
|
||||
"perf:test": "NODE_ENV=test maestro test",
|
||||
"perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml",
|
||||
@@ -71,7 +72,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.17.0",
|
||||
"@atproto/api": "^0.17.1",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@bsky.app/alf": "^0.1.5",
|
||||
@@ -220,7 +221,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.172",
|
||||
"@atproto/dev-env": "^0.3.181",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
@@ -272,6 +273,7 @@
|
||||
"react-refresh": "^0.14.0",
|
||||
"svgo": "^3.3.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-plugin-sort-import-suggestions": "^1.0.4",
|
||||
"typescript": "^5.9.2",
|
||||
"webpack-bundle-analyzer": "^4.10.1"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
diff --git a/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift b/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
|
||||
index c34ba71..13d576a 100644
|
||||
--- a/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
|
||||
+++ b/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
|
||||
@@ -159,13 +159,23 @@ class RNUITextViewShadow: RCTShadowView {
|
||||
let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
|
||||
let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
|
||||
|
||||
- var totalLines = self.lineHeight == 0.0 ? 0 : Int(ceil(textSize.height / self.lineHeight))
|
||||
-
|
||||
- if self.numberOfLines != 0, totalLines > self.numberOfLines {
|
||||
- totalLines = self.numberOfLines
|
||||
+ var finalHeight: CGFloat
|
||||
+
|
||||
+ if self.numberOfLines != 0 && self.lineHeight != 0.0 {
|
||||
+ // numberOfLines is set with custom line height - need to calculate lines and snap to lineHeight multiples
|
||||
+ // NOTE: this calculation can be inaccurate with fractional font sizes
|
||||
+ var totalLines = Int(ceil(textSize.height / self.lineHeight))
|
||||
+ if totalLines > self.numberOfLines {
|
||||
+ totalLines = self.numberOfLines
|
||||
+ }
|
||||
+ finalHeight = CGFloat(totalLines) * self.lineHeight
|
||||
+ } else {
|
||||
+ // Either no numberOfLines limit, or no custom lineHeight - use actual text height
|
||||
+ // (numberOfLines without custom lineHeight is handled by the UITextView's textContainer.maximumNumberOfLines)
|
||||
+ finalHeight = textSize.height
|
||||
}
|
||||
|
||||
- self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight))
|
||||
+ self.frameSize = CGSize(width: CGFloat(maxWidth), height: finalHeight)
|
||||
return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height))
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components
|
||||
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
|
||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||
import {Text as BaseText, type TextProps} from '#/components/Typography'
|
||||
import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSadIcon} from './icons/Emoji'
|
||||
|
||||
export const colors = {
|
||||
warning: '#FFC404',
|
||||
}
|
||||
|
||||
type Context = {
|
||||
type: 'info' | 'tip' | 'warning' | 'error'
|
||||
type: 'info' | 'tip' | 'warning' | 'error' | 'apology'
|
||||
}
|
||||
|
||||
const Context = createContext<Context>({
|
||||
@@ -29,12 +30,14 @@ export function Icon() {
|
||||
tip: CircleInfoIcon,
|
||||
warning: WarningIcon,
|
||||
error: CircleXIcon,
|
||||
apology: EmojiSadIcon,
|
||||
}[type]
|
||||
const fill = {
|
||||
info: t.atoms.text_contrast_medium.color,
|
||||
tip: t.palette.primary_500,
|
||||
warning: colors.warning,
|
||||
error: t.palette.negative_500,
|
||||
apology: t.atoms.text_contrast_medium.color,
|
||||
}[type]
|
||||
return <Icon fill={fill} size="md" />
|
||||
}
|
||||
@@ -109,6 +112,7 @@ export function Outer({
|
||||
tip: t.palette.primary_500,
|
||||
warning: colors.warning,
|
||||
error: t.palette.negative_500,
|
||||
apology: t.atoms.border_contrast_high.borderColor,
|
||||
}[type]
|
||||
return (
|
||||
<Context.Provider value={{type}}>
|
||||
|
||||
@@ -390,7 +390,12 @@ export function ProfileGrid({
|
||||
{!isProfileHeaderContext && (
|
||||
<InlineLinkText
|
||||
label={_(msg`See more suggested profiles on the Explore page`)}
|
||||
to="/search">
|
||||
to="/search"
|
||||
onPress={() => {
|
||||
logger.metric('suggestedUser:seeMore', {
|
||||
logContext: isFeedContext ? 'Explore' : 'Profile',
|
||||
})
|
||||
}}>
|
||||
<Trans>See more</Trans>
|
||||
</InlineLinkText>
|
||||
)}
|
||||
@@ -429,6 +434,11 @@ function SeeMoreSuggestedProfilesCard() {
|
||||
to="/search"
|
||||
color="primary"
|
||||
label={_(msg`Browse more accounts on the Explore page`)}
|
||||
onPress={() => {
|
||||
logger.metric('suggestedUser:seeMore', {
|
||||
logContext: 'Explore',
|
||||
})
|
||||
}}
|
||||
style={[
|
||||
a.flex_col,
|
||||
a.align_center,
|
||||
|
||||
@@ -23,7 +23,7 @@ import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
||||
import {SubtleHover} from '#/components/SubtleHover'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {
|
||||
type Embed as TEmbed,
|
||||
@@ -284,7 +284,7 @@ export function QuoteEmbed({
|
||||
childContainerStyle={[a.pt_sm]}>
|
||||
{({active}) => (
|
||||
<>
|
||||
{!active && <SubtleWebHover hover={hover} style={[a.rounded_md]} />}
|
||||
{!active && <SubtleHover hover={hover} style={[a.rounded_md]} />}
|
||||
<Link
|
||||
style={[!active && a.p_md]}
|
||||
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
||||
|
||||
@@ -57,7 +57,9 @@ export function PostRepliedTo({
|
||||
size="xs"
|
||||
style={[t.atoms.text_contrast_medium, {top: -1}]}
|
||||
/>
|
||||
<Text style={textStyle}>{label}</Text>
|
||||
<Text style={[a.flex_1, textStyle]} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,6 +258,7 @@ let PostMenuItems = ({
|
||||
|
||||
const onHidePost = () => {
|
||||
hidePost({uri: postUri})
|
||||
logEvent('thread:click:hideReplyForMe', {})
|
||||
}
|
||||
|
||||
const hideInPWI = !!postAuthor.labels?.find(
|
||||
@@ -338,6 +339,12 @@ let PostMenuItems = ({
|
||||
replyUri: postUri,
|
||||
action,
|
||||
})
|
||||
|
||||
// Log metric only when hiding (not when showing)
|
||||
if (isHide) {
|
||||
logEvent('thread:click:hideReplyForEveryone', {})
|
||||
}
|
||||
|
||||
Toast.show(
|
||||
isHide
|
||||
? _(msg`Reply was successfully hidden`)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {type ModerationOpts} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
@@ -13,10 +14,6 @@ import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggeste
|
||||
import {useSession} from '#/state/session'
|
||||
import {type Follow10ProgressGuide} from '#/state/shell/progress-guide'
|
||||
import {type ListMethods} from '#/view/com/util/List'
|
||||
import {
|
||||
popularInterests,
|
||||
useInterestsDisplayNames,
|
||||
} from '#/screens/Onboarding/state'
|
||||
import {
|
||||
atoms as a,
|
||||
native,
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {isTouchDevice} from '#/lib/browser'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function SubtleHover({style, hover}: ViewStyleProp & {hover: boolean}) {
|
||||
export function SubtleHover({
|
||||
style,
|
||||
hover,
|
||||
web = true,
|
||||
native = false,
|
||||
}: ViewStyleProp & {hover: boolean; web?: boolean; native?: boolean}) {
|
||||
const t = useTheme()
|
||||
|
||||
let opacity: number
|
||||
@@ -18,17 +25,25 @@ export function SubtleHover({style, hover}: ViewStyleProp & {hover: boolean}) {
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
const el = (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.pointer_events_none,
|
||||
a.transition_opacity,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.bg_contrast_50,
|
||||
style,
|
||||
{opacity: hover ? opacity : 0},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
if (isWeb && web) {
|
||||
return isTouchDevice ? null : el
|
||||
} else if (isNative && native) {
|
||||
return el
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import {type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function SubtleWebHover({}: ViewStyleProp & {hover: boolean}) {
|
||||
return null
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
|
||||
import {isTouchDevice} from '#/lib/browser'
|
||||
import {useTheme, type ViewStyleProp} from '#/alf'
|
||||
|
||||
export function SubtleWebHover({
|
||||
style,
|
||||
hover,
|
||||
}: ViewStyleProp & {hover: boolean}) {
|
||||
const t = useTheme()
|
||||
if (isTouchDevice) {
|
||||
return null
|
||||
}
|
||||
let opacity = 0.5
|
||||
switch (t.name) {
|
||||
case 'dark':
|
||||
opacity = 0.4
|
||||
break
|
||||
case 'dim':
|
||||
opacity = 0.45
|
||||
break
|
||||
case 'light':
|
||||
opacity = 0.5
|
||||
break
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg_contrast_25,
|
||||
styles.container,
|
||||
{opacity: hover ? opacity : 0},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
pointerEvents: 'none',
|
||||
// @ts-ignore web only
|
||||
transition: '0.15s ease-in-out opacity',
|
||||
},
|
||||
})
|
||||
@@ -300,7 +300,7 @@ export function GifPreview({
|
||||
a.flex_1,
|
||||
a.mb_sm,
|
||||
a.rounded_sm,
|
||||
a.aspect_card,
|
||||
a.aspect_square,
|
||||
{opacity: pressed ? 0.8 : 1},
|
||||
t.atoms.bg_contrast_25,
|
||||
]}
|
||||
|
||||
@@ -13,6 +13,7 @@ import isEqual from 'lodash.isequal'
|
||||
import {logger} from '#/logger'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {useMyListsQuery} from '#/state/queries/my-lists'
|
||||
import {useGetPost} from '#/state/queries/post'
|
||||
import {
|
||||
createPostgateQueryKey,
|
||||
getPostgateRecord,
|
||||
@@ -25,12 +26,15 @@ import {
|
||||
} from '#/state/queries/postgate/util'
|
||||
import {
|
||||
createThreadgateViewQueryKey,
|
||||
getThreadgateView,
|
||||
type ThreadgateAllowUISetting,
|
||||
threadgateViewToAllowUISetting,
|
||||
useSetThreadgateAllowMutation,
|
||||
useThreadgateViewQuery,
|
||||
} from '#/state/queries/threadgate'
|
||||
import {
|
||||
PostThreadContextProvider,
|
||||
usePostThreadContext,
|
||||
} from '#/state/queries/usePostThread'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -133,10 +137,13 @@ export type PostInteractionSettingsDialogProps = {
|
||||
export function PostInteractionSettingsDialog(
|
||||
props: PostInteractionSettingsDialogProps,
|
||||
) {
|
||||
const postThreadContext = usePostThreadContext()
|
||||
return (
|
||||
<Dialog.Outer control={props.control}>
|
||||
<Dialog.Handle />
|
||||
<PostInteractionSettingsDialogControlledInner {...props} />
|
||||
<PostThreadContextProvider context={postThreadContext}>
|
||||
<PostInteractionSettingsDialogControlledInner {...props} />
|
||||
</PostThreadContextProvider>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
@@ -558,6 +565,7 @@ export function usePrefetchPostInteractionSettings({
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
const getPost = useGetPost()
|
||||
|
||||
return React.useCallback(async () => {
|
||||
try {
|
||||
@@ -570,7 +578,10 @@ export function usePrefetchPostInteractionSettings({
|
||||
}),
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: createThreadgateViewQueryKey(rootPostUri),
|
||||
queryFn: () => getThreadgateView({agent, postUri: rootPostUri}),
|
||||
queryFn: async () => {
|
||||
const post = await getPost({uri: rootPostUri})
|
||||
return post.threadgate ?? null
|
||||
},
|
||||
staleTime: STALE.SECONDS.THIRTY,
|
||||
}),
|
||||
])
|
||||
@@ -579,5 +590,5 @@ export function usePrefetchPostInteractionSettings({
|
||||
safeMessage: e.message,
|
||||
})
|
||||
}
|
||||
}, [queryClient, agent, postUri, rootPostUri])
|
||||
}, [queryClient, agent, postUri, rootPostUri, getPost])
|
||||
}
|
||||
|
||||
@@ -435,7 +435,7 @@ function DialogInner({
|
||||
multiline
|
||||
label={_(msg`Description`)}
|
||||
placeholder={descriptionPlaceholder}
|
||||
testID="editProfileDescriptionInput"
|
||||
testID="editListDescriptionInput"
|
||||
/>
|
||||
</TextField.Root>
|
||||
{descriptionTooLong && (
|
||||
|
||||
@@ -56,10 +56,7 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
||||
|
||||
const date = new Date()
|
||||
date.setMinutes(date.getMinutes() + offset)
|
||||
return i18n
|
||||
.date(date, {hour: 'numeric', minute: '2-digit', hour12: true})
|
||||
.toLocaleUpperCase()
|
||||
.replace(' ', '')
|
||||
return i18n.date(date, {hour: 'numeric', minute: '2-digit', hour12: true})
|
||||
},
|
||||
[tick, i18n],
|
||||
)
|
||||
|
||||
@@ -78,6 +78,7 @@ const NETWORK_ERRORS = [
|
||||
'Network request failed',
|
||||
'Failed to fetch',
|
||||
'Load failed',
|
||||
'Upstream service unreachable',
|
||||
]
|
||||
|
||||
export function isNetworkError(e: unknown) {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import {useMemo} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const interests = [
|
||||
'animals',
|
||||
'art',
|
||||
'books',
|
||||
'comedy',
|
||||
'comics',
|
||||
'culture',
|
||||
'dev',
|
||||
'education',
|
||||
'finance',
|
||||
'food',
|
||||
'gaming',
|
||||
'journalism',
|
||||
'movies',
|
||||
'music',
|
||||
'nature',
|
||||
'news',
|
||||
'pets',
|
||||
'photography',
|
||||
'politics',
|
||||
'science',
|
||||
'sports',
|
||||
'tech',
|
||||
'tv',
|
||||
'writers',
|
||||
] as const
|
||||
export type Interest = (typeof interests)[number]
|
||||
|
||||
// most popular selected interests
|
||||
export const popularInterests = [
|
||||
'art',
|
||||
'gaming',
|
||||
'sports',
|
||||
'comics',
|
||||
'music',
|
||||
'politics',
|
||||
'photography',
|
||||
'science',
|
||||
'news',
|
||||
] satisfies Interest[]
|
||||
|
||||
export function useInterestsDisplayNames() {
|
||||
const {_} = useLingui()
|
||||
|
||||
return useMemo<Record<string, string>>(() => {
|
||||
return {
|
||||
// Keep this alphabetized
|
||||
animals: _(msg`Animals`),
|
||||
art: _(msg`Art`),
|
||||
books: _(msg`Books`),
|
||||
comedy: _(msg`Comedy`),
|
||||
comics: _(msg`Comics`),
|
||||
culture: _(msg`Culture`),
|
||||
dev: _(msg`Software Dev`),
|
||||
education: _(msg`Education`),
|
||||
finance: _(msg`Finance`),
|
||||
food: _(msg`Food`),
|
||||
gaming: _(msg`Video Games`),
|
||||
journalism: _(msg`Journalism`),
|
||||
movies: _(msg`Movies`),
|
||||
music: _(msg`Music`),
|
||||
nature: _(msg`Nature`),
|
||||
news: _(msg`News`),
|
||||
pets: _(msg`Pets`),
|
||||
photography: _(msg`Photography`),
|
||||
politics: _(msg`Politics`),
|
||||
science: _(msg`Science`),
|
||||
sports: _(msg`Sports`),
|
||||
tech: _(msg`Tech`),
|
||||
tv: _(msg`TV`),
|
||||
writers: _(msg`Writers`),
|
||||
} satisfies Record<Interest, string>
|
||||
}, [_])
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import {type ModerationUI} from '@atproto/api'
|
||||
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
@@ -5,9 +7,10 @@ import type * as bsky from '#/types/bsky'
|
||||
export function createSanitizedDisplayName(
|
||||
profile: bsky.profile.AnyProfileView,
|
||||
noAt = false,
|
||||
moderation?: ModerationUI,
|
||||
) {
|
||||
if (profile.displayName != null && profile.displayName !== '') {
|
||||
return sanitizeDisplayName(profile.displayName)
|
||||
return sanitizeDisplayName(profile.displayName, moderation)
|
||||
} else {
|
||||
return sanitizeHandle(profile.handle, noAt ? '' : '@')
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import debounce from 'lodash.debounce'
|
||||
|
||||
import {PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID} from '#/lib/constants'
|
||||
import {logger as notyLogger} from '#/lib/notifications/util'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useAgeAssuranceContext} from '#/state/ageAssurance'
|
||||
import {type SessionAccount, useAgent, useSession} from '#/state/session'
|
||||
@@ -47,7 +48,9 @@ async function _registerPushToken({
|
||||
|
||||
notyLogger.debug(`registerPushToken: success`)
|
||||
} catch (error) {
|
||||
notyLogger.error(`registerPushToken: failed`, {safeMessage: error})
|
||||
if (!isNetworkError(error)) {
|
||||
notyLogger.error(`registerPushToken: failed`, {safeMessage: error})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ export type Gate =
|
||||
| 'alt_share_icon'
|
||||
| 'debug_show_feedcontext'
|
||||
| 'debug_subscriptions'
|
||||
| 'disable_onboarding_policy_update_notice'
|
||||
| 'explore_show_suggested_feeds'
|
||||
| 'feed_reply_button_open_thread'
|
||||
| 'old_postonboarding'
|
||||
| 'onboarding_add_video_feed'
|
||||
| 'onboarding_suggested_starterpacks'
|
||||
| 'post_follow_profile_suggested_accounts'
|
||||
| 'remove_show_latest_button'
|
||||
| 'test_gate_1'
|
||||
|
||||
@@ -31,6 +31,7 @@ const NETWORK_ERRORS = [
|
||||
'Network request failed',
|
||||
'Failed to fetch',
|
||||
'Load failed',
|
||||
'Upstream service unreachable',
|
||||
]
|
||||
|
||||
export function isNetworkError(e: unknown) {
|
||||
|
||||
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1433
-1426
File diff suppressed because it is too large
Load Diff
+1434
-1427
File diff suppressed because it is too large
Load Diff
+1433
-1426
File diff suppressed because it is too large
Load Diff
+1433
-1426
File diff suppressed because it is too large
Load Diff
+1468
-1461
File diff suppressed because it is too large
Load Diff
+1436
-1429
File diff suppressed because it is too large
Load Diff
+1442
-1435
File diff suppressed because it is too large
Load Diff
+1441
-1434
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1436
-1429
File diff suppressed because it is too large
Load Diff
+516
-507
File diff suppressed because it is too large
Load Diff
+1445
-1438
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1436
-1429
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1434
-1427
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1502
-1495
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1439
-1432
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1484
-1477
File diff suppressed because it is too large
Load Diff
+1490
-1483
File diff suppressed because it is too large
Load Diff
+1435
-1428
File diff suppressed because it is too large
Load Diff
+1433
-1426
File diff suppressed because it is too large
Load Diff
+1435
-1428
File diff suppressed because it is too large
Load Diff
+1437
-1430
File diff suppressed because it is too large
Load Diff
+1433
-1426
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1455
-1448
File diff suppressed because it is too large
Load Diff
+1463
-1456
File diff suppressed because it is too large
Load Diff
+1561
-1554
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1456
-1449
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1440
-1433
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1438
-1431
File diff suppressed because it is too large
Load Diff
+1442
-1435
File diff suppressed because it is too large
Load Diff
+1446
-1439
File diff suppressed because it is too large
Load Diff
+1451
-1444
File diff suppressed because it is too large
Load Diff
@@ -297,6 +297,14 @@ export type MetricEvents = {
|
||||
recId?: number
|
||||
position: number
|
||||
}
|
||||
'suggestedUser:seeMore': {
|
||||
logContext:
|
||||
| 'Explore'
|
||||
| 'InterstitialDiscover'
|
||||
| 'InterstitialProfile'
|
||||
| 'Profile'
|
||||
| 'Onboarding'
|
||||
}
|
||||
'profile:unfollow': {
|
||||
logContext:
|
||||
| 'RecommendedFollowsItem'
|
||||
@@ -481,6 +489,8 @@ export type MetricEvents = {
|
||||
'share:press:embed': {}
|
||||
|
||||
'thread:click:showOtherReplies': {}
|
||||
'thread:click:hideReplyForMe': {}
|
||||
'thread:click:hideReplyForEveryone': {}
|
||||
'thread:preferences:load': {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -29,5 +29,5 @@ init({
|
||||
* @see https://docs.sentry.io/platforms/react-native/configuration/options/#attach-stacktrace
|
||||
*/
|
||||
attachStacktrace: false,
|
||||
sampleRate: env.IS_INTERNAL ? 1.0 : 0.1,
|
||||
// sampleRate: env.IS_INTERNAL ? 1.0 : 0.1,
|
||||
})
|
||||
|
||||
+53
-2
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {type ListRenderItemInfo, View} from 'react-native'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
@@ -14,16 +14,22 @@ import {cleanError} from '#/lib/strings/errors'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {enforceLen} from '#/lib/strings/helpers'
|
||||
import {useSearchPostsQuery} from '#/state/queries/search-posts'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import {Pager} from '#/view/com/pager/Pager'
|
||||
import {TabBar} from '#/view/com/pager/TabBar'
|
||||
import {Post} from '#/view/com/post/Post'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {atoms as a, web} from '#/alf'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {SearchError} from '#/components/SearchError'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const renderItem = ({item}: ListRenderItemInfo<AppBskyFeedDefs.PostView>) => {
|
||||
return <Post post={item} />
|
||||
@@ -161,6 +167,8 @@ function HashtagScreenTab({
|
||||
const {_} = useLingui()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
const [isPTR, setIsPTR] = React.useState(false)
|
||||
const t = useTheme()
|
||||
const {hasSession} = useSession()
|
||||
|
||||
const queryParam = React.useMemo(() => {
|
||||
if (!author) return fullTag
|
||||
@@ -194,6 +202,49 @@ function HashtagScreenTab({
|
||||
fetchNextPage()
|
||||
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
|
||||
|
||||
const closeAllActiveElements = useCloseAllActiveElements()
|
||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||
|
||||
const showSignIn = () => {
|
||||
closeAllActiveElements()
|
||||
requestSwitchToAccount({requestedAccount: 'none'})
|
||||
}
|
||||
|
||||
const showCreateAccount = () => {
|
||||
closeAllActiveElements()
|
||||
requestSwitchToAccount({requestedAccount: 'new'})
|
||||
}
|
||||
|
||||
if (!hasSession) {
|
||||
return (
|
||||
<SearchError
|
||||
title={_(msg`Search is currently unavailable when logged out`)}>
|
||||
<Text style={[a.text_md, a.text_center, a.leading_snug]}>
|
||||
<Trans>
|
||||
<InlineLinkText
|
||||
label={_(msg`Sign in`)}
|
||||
to={'#'}
|
||||
onPress={showSignIn}>
|
||||
Sign in
|
||||
</InlineLinkText>
|
||||
<Text style={t.atoms.text_contrast_medium}> or </Text>
|
||||
<InlineLinkText
|
||||
label={_(msg`Create an account`)}
|
||||
to={'#'}
|
||||
onPress={showCreateAccount}>
|
||||
create an account
|
||||
</InlineLinkText>
|
||||
<Text> </Text>
|
||||
<Text style={t.atoms.text_contrast_medium}>
|
||||
to search for news, sports, politics, and everything else
|
||||
happening on Bluesky.
|
||||
</Text>
|
||||
</Trans>
|
||||
</Text>
|
||||
</SearchError>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{posts.length < 1 ? (
|
||||
|
||||
@@ -328,19 +328,21 @@ export function ModerationScreenInner({
|
||||
</Link>
|
||||
</View>
|
||||
|
||||
{declaredAge === undefined && (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
a.pt_2xl,
|
||||
a.pb_md,
|
||||
a.text_md,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Content filters</Trans>
|
||||
</Text>
|
||||
{(!isDeclaredUnderage || declaredAge === undefined) && (
|
||||
<Text
|
||||
style={[
|
||||
a.pt_2xl,
|
||||
a.pb_md,
|
||||
a.text_md,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Content filters</Trans>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{declaredAge === undefined ? (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Confirm your birthdate`)}
|
||||
size="small"
|
||||
@@ -360,21 +362,8 @@ export function ModerationScreenInner({
|
||||
|
||||
<BirthDateSettingsDialog control={birthdateDialogControl} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isDeclaredUnderage && (
|
||||
) : !isDeclaredUnderage ? (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
a.pt_2xl,
|
||||
a.pb_md,
|
||||
a.text_md,
|
||||
a.font_semi_bold,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Content filters</Trans>
|
||||
</Text>
|
||||
|
||||
<AgeAssuranceAdmonition style={[a.pb_md]}>
|
||||
<Trans>
|
||||
You must complete age assurance in order to access the settings
|
||||
@@ -466,7 +455,7 @@ export function ModerationScreenInner({
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<Text
|
||||
style={[
|
||||
|
||||
@@ -330,6 +330,7 @@ function ValueProposition({
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
testID="onboardingFinish"
|
||||
disabled={saving}
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
color="primary"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react'
|
||||
import {type TextStyle, View, type ViewStyle} from 'react-native'
|
||||
|
||||
import {type Interest, useInterestsDisplayNames} from '#/lib/interests'
|
||||
import {capitalize} from '#/lib/strings/capitalize'
|
||||
import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function InterestButton({interest}: {interest: string}) {
|
||||
export function InterestButton({interest}: {interest: Interest}) {
|
||||
const t = useTheme()
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
const ctx = Toggle.useItemContext()
|
||||
|
||||
@@ -2,97 +2,35 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {interests, useInterestsDisplayNames} from '#/lib/interests'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {capitalize} from '#/lib/strings/capitalize'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {
|
||||
DescriptionText,
|
||||
OnboardingControls,
|
||||
TitleText,
|
||||
} from '#/screens/Onboarding/Layout'
|
||||
import {
|
||||
type ApiResponseMap,
|
||||
Context,
|
||||
useInterestsDisplayNames,
|
||||
} from '#/screens/Onboarding/state'
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {IconCircle} from '#/components/IconCircle'
|
||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwise} from '#/components/icons/ArrowRotateCounterClockwise'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSad} from '#/components/icons/Emoji'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function StepInterests() {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
const [saving, setSaving] = React.useState(false)
|
||||
const [interests, setInterests] = React.useState<string[]>(
|
||||
const [selectedInterests, setSelectedInterests] = React.useState<string[]>(
|
||||
state.interestsStepResults.selectedInterests.map(i => i),
|
||||
)
|
||||
const onboardDispatch = useOnboardingDispatch()
|
||||
const agent = useAgent()
|
||||
const {isLoading, isError, error, data, refetch, isFetching} = useQuery({
|
||||
queryKey: ['interests'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const {data} = await agent.app.bsky.unspecced.getTaggedSuggestions()
|
||||
return data.suggestions.reduce(
|
||||
(agg, s) => {
|
||||
const {tag, subject, subjectType} = s
|
||||
const isDefault = tag === 'default'
|
||||
|
||||
if (!agg.interests.includes(tag) && !isDefault) {
|
||||
agg.interests.push(tag)
|
||||
}
|
||||
|
||||
if (subjectType === 'user') {
|
||||
agg.suggestedAccountDids[tag] =
|
||||
agg.suggestedAccountDids[tag] || []
|
||||
agg.suggestedAccountDids[tag].push(subject)
|
||||
}
|
||||
|
||||
if (subjectType === 'feed') {
|
||||
// agg all feeds into defaults
|
||||
if (isDefault) {
|
||||
agg.suggestedFeedUris[tag] = agg.suggestedFeedUris[tag] || []
|
||||
} else {
|
||||
agg.suggestedFeedUris[tag] = agg.suggestedFeedUris[tag] || []
|
||||
agg.suggestedFeedUris[tag].push(subject)
|
||||
agg.suggestedFeedUris.default.push(subject)
|
||||
}
|
||||
}
|
||||
|
||||
return agg
|
||||
},
|
||||
{
|
||||
interests: [],
|
||||
suggestedAccountDids: {},
|
||||
suggestedFeedUris: {},
|
||||
} as ApiResponseMap,
|
||||
)
|
||||
} catch (e: any) {
|
||||
logger.info(
|
||||
`onboarding: getTaggedSuggestions fetch or processing failed`,
|
||||
)
|
||||
logger.error(e)
|
||||
|
||||
throw new Error(`a network error occurred`)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const saveInterests = React.useCallback(async () => {
|
||||
setSaving(true)
|
||||
@@ -101,164 +39,62 @@ export function StepInterests() {
|
||||
setSaving(false)
|
||||
dispatch({
|
||||
type: 'setInterestsStepResults',
|
||||
apiResponse: data!,
|
||||
selectedInterests: interests,
|
||||
selectedInterests,
|
||||
})
|
||||
dispatch({type: 'next'})
|
||||
logEvent('onboarding:interests:nextPressed', {
|
||||
selectedInterests: interests,
|
||||
selectedInterestsLength: interests.length,
|
||||
selectedInterests,
|
||||
selectedInterestsLength: selectedInterests.length,
|
||||
})
|
||||
} catch (e: any) {
|
||||
logger.info(`onboading: error saving interests`)
|
||||
logger.error(e)
|
||||
}
|
||||
}, [interests, data, setSaving, dispatch])
|
||||
|
||||
const skipOnboarding = React.useCallback(() => {
|
||||
onboardDispatch({type: 'finish'})
|
||||
dispatch({type: 'finish'})
|
||||
}, [onboardDispatch, dispatch])
|
||||
|
||||
const title = isError ? (
|
||||
<Trans>Oh no! Something went wrong.</Trans>
|
||||
) : (
|
||||
<Trans>What are your interests?</Trans>
|
||||
)
|
||||
const description = isError ? (
|
||||
<Trans>
|
||||
We weren't able to connect. Please try again to continue setting up your
|
||||
account. If it continues to fail, you can skip this flow.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>We'll use this to help customize your experience.</Trans>
|
||||
)
|
||||
}, [selectedInterests, setSaving, dispatch])
|
||||
|
||||
return (
|
||||
<View style={[a.align_start]} testID="onboardingInterests">
|
||||
<IconCircle
|
||||
icon={isError ? EmojiSad : Hashtag}
|
||||
style={[
|
||||
a.mb_2xl,
|
||||
isError
|
||||
? {
|
||||
backgroundColor: t.palette.negative_50,
|
||||
}
|
||||
: {},
|
||||
]}
|
||||
iconStyle={[
|
||||
isError
|
||||
? {
|
||||
color: t.palette.negative_900,
|
||||
}
|
||||
: {},
|
||||
]}
|
||||
/>
|
||||
<IconCircle icon={Hashtag} style={[a.mb_2xl]} />
|
||||
|
||||
<TitleText>{title}</TitleText>
|
||||
<DescriptionText>{description}</DescriptionText>
|
||||
<TitleText>
|
||||
<Trans>What are your interests?</Trans>
|
||||
</TitleText>
|
||||
<DescriptionText>
|
||||
<Trans>We'll use this to help customize your experience.</Trans>
|
||||
</DescriptionText>
|
||||
|
||||
<View style={[a.w_full, a.pt_2xl]}>
|
||||
{isLoading ? (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.mt_md,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{minHeight: 400},
|
||||
]}>
|
||||
<Loader size="xl" />
|
||||
<Toggle.Group
|
||||
values={selectedInterests}
|
||||
onChange={setSelectedInterests}
|
||||
label={_(msg`Select your interests from the options below`)}>
|
||||
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
|
||||
{interests.map(interest => (
|
||||
<Toggle.Item
|
||||
key={interest}
|
||||
name={interest}
|
||||
label={interestsDisplayNames[interest] || capitalize(interest)}>
|
||||
<InterestButton interest={interest} />
|
||||
</Toggle.Item>
|
||||
))}
|
||||
</View>
|
||||
) : isError || !data ? (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.p_lg,
|
||||
a.rounded_md,
|
||||
{
|
||||
backgroundColor: t.palette.negative_50,
|
||||
},
|
||||
]}>
|
||||
<Text style={[a.text_md]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_semi_bold,
|
||||
{
|
||||
color: t.palette.negative_900,
|
||||
},
|
||||
]}>
|
||||
<Trans>Error:</Trans>{' '}
|
||||
</Text>
|
||||
{error?.message || _(msg`an unknown error occurred`)}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Toggle.Group
|
||||
values={interests}
|
||||
onChange={setInterests}
|
||||
label={_(msg`Select your interests from the options below`)}>
|
||||
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
|
||||
{data.interests.map(interest => (
|
||||
<Toggle.Item
|
||||
key={interest}
|
||||
name={interest}
|
||||
label={
|
||||
interestsDisplayNames[interest] || capitalize(interest)
|
||||
}>
|
||||
<InterestButton interest={interest} />
|
||||
</Toggle.Item>
|
||||
))}
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
)}
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
{isError ? (
|
||||
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
||||
<Button
|
||||
disabled={isFetching}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="large"
|
||||
label={_(msg`Retry`)}
|
||||
onPress={() => refetch()}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={ArrowRotateCounterClockwise} position="right" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="secondary"
|
||||
size="large"
|
||||
label={_(msg`Skip this flow`)}
|
||||
onPress={skipOnboarding}>
|
||||
<ButtonText>
|
||||
<Trans>Skip</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<Button
|
||||
disabled={saving || !data}
|
||||
testID="onboardingContinue"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
label={_(msg`Continue to next step`)}
|
||||
onPress={saveInterests}>
|
||||
<ButtonText>
|
||||
<Trans>Continue</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon
|
||||
icon={saving ? Loader : ChevronRight}
|
||||
position="right"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={saving}
|
||||
testID="onboardingContinue"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
label={_(msg`Continue to next step`)}
|
||||
onPress={saveInterests}>
|
||||
<ButtonText>
|
||||
<Trans>Continue</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={saving ? Loader : ChevronRight} />
|
||||
</Button>
|
||||
</OnboardingControls.Portal>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useContext, useMemo, useState} from 'react'
|
||||
import {useContext, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type ModerationOpts} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
@@ -7,6 +7,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
import * as bcp47Match from 'bcp-47-match'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
|
||||
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
@@ -14,13 +15,8 @@ import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {OnboardingControls} from '#/screens/Onboarding/Layout'
|
||||
import {
|
||||
Context,
|
||||
popularInterests,
|
||||
useInterestsDisplayNames,
|
||||
} from '#/screens/Onboarding/state'
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers'
|
||||
import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
@@ -45,7 +41,6 @@ export function StepSuggestedAccounts() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const {state, dispatch} = useContext(Context)
|
||||
const onboardDispatch = useOnboardingDispatch()
|
||||
|
||||
const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
|
||||
// keeping track of who was followed via the follow all button
|
||||
@@ -77,11 +72,8 @@ export function StepSuggestedAccounts() {
|
||||
})
|
||||
|
||||
const isError = !!error
|
||||
|
||||
const skipOnboarding = useCallback(() => {
|
||||
onboardDispatch({type: 'finish'})
|
||||
dispatch({type: 'finish'})
|
||||
}, [onboardDispatch, dispatch])
|
||||
const isEmpty =
|
||||
!isLoading && suggestedUsers && suggestedUsers.actors.length === 0
|
||||
|
||||
const followableDids =
|
||||
suggestedUsers?.actors
|
||||
@@ -171,13 +163,21 @@ export function StepSuggestedAccounts() {
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
) : isError ? (
|
||||
<View style={[a.flex_1, a.px_xl, a.pt_5xl]}>
|
||||
<View style={[a.flex_1, a.px_xl, a.pt_2xl]}>
|
||||
<Admonition type="error">
|
||||
<Trans>
|
||||
An error occurred while fetching suggested accounts.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
</View>
|
||||
) : isEmpty ? (
|
||||
<View style={[a.flex_1, a.px_xl, a.pt_2xl]}>
|
||||
<Admonition type="apology">
|
||||
<Trans>
|
||||
Sorry, we're unable to load account suggestions at this time.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
@@ -216,8 +216,8 @@ export function StepSuggestedAccounts() {
|
||||
<Button
|
||||
color="secondary"
|
||||
size="large"
|
||||
label={_(msg`Skip this flow`)}
|
||||
onPress={skipOnboarding}>
|
||||
label={_(msg`Skip to next step`)}
|
||||
onPress={() => dispatch({type: 'next'})}>
|
||||
<ButtonText>
|
||||
<Trans>Skip</Trans>
|
||||
</ButtonText>
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {batchedUpdates} from '#/lib/batchedUpdates'
|
||||
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
||||
import {logger} from '#/logger'
|
||||
import {updateProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {getAllListMembers} from '#/state/queries/list-members'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {bulkWriteFollows} from '#/screens/Onboarding/util'
|
||||
import {AvatarStack} from '#/screens/Search/components/StarterPackCard'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const IGNORED_ACCOUNT = 'did:plc:pifkcjimdcfwaxkanzhwxufp'
|
||||
|
||||
export function StarterPackCard({
|
||||
view,
|
||||
}: {
|
||||
view: AppBskyGraphDefs.StarterPackView
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const record = view.record
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [isFollowingAll, setIsFollowingAll] = useState(false)
|
||||
|
||||
const onFollowAll = async () => {
|
||||
if (!view.list) return
|
||||
|
||||
setIsProcessing(true)
|
||||
|
||||
let listItems: AppBskyGraphDefs.ListItemView[] = []
|
||||
try {
|
||||
listItems = await getAllListMembers(agent, view.list.uri)
|
||||
} catch (e) {
|
||||
setIsProcessing(false)
|
||||
Toast.show(_(msg`An error occurred while trying to follow all`), {
|
||||
type: 'error',
|
||||
})
|
||||
logger.error('Failed to get list members for starter pack', {
|
||||
safeMessage: e,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const dids = listItems
|
||||
.filter(
|
||||
li =>
|
||||
li.subject.did !== IGNORED_ACCOUNT &&
|
||||
li.subject.did !== currentAccount?.did &&
|
||||
!isBlockedOrBlocking(li.subject) &&
|
||||
!isMuted(li.subject) &&
|
||||
!li.subject.viewer?.following,
|
||||
)
|
||||
.map(li => li.subject.did)
|
||||
|
||||
let followUris: Map<string, string>
|
||||
try {
|
||||
followUris = await bulkWriteFollows(agent, dids)
|
||||
} catch (e) {
|
||||
setIsProcessing(false)
|
||||
Toast.show(_(msg`An error occurred while trying to follow all`), {
|
||||
type: 'error',
|
||||
})
|
||||
logger.error('Failed to follow all accounts', {safeMessage: e})
|
||||
}
|
||||
|
||||
setIsFollowingAll(true)
|
||||
setIsProcessing(false)
|
||||
batchedUpdates(() => {
|
||||
for (let did of dids) {
|
||||
updateProfileShadow(queryClient, did, {
|
||||
followingUri: followUris.get(did),
|
||||
})
|
||||
}
|
||||
})
|
||||
Toast.show(_(msg`All accounts have been followed!`), {type: 'success'})
|
||||
logger.metric('starterPack:followAll', {
|
||||
logContext: 'Onboarding',
|
||||
starterPack: view.uri,
|
||||
count: dids.length,
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
|
||||
record,
|
||||
AppBskyGraphStarterpack.isRecord,
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const profileCount = gtPhone ? 11 : 8
|
||||
const profiles = view.listItemsSample
|
||||
?.slice(0, profileCount)
|
||||
.map(item => item.subject)
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.p_lg,
|
||||
a.gap_md,
|
||||
a.border,
|
||||
a.rounded_lg,
|
||||
a.overflow_hidden,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}>
|
||||
<AvatarStack
|
||||
profiles={profiles ?? []}
|
||||
numPending={profileCount}
|
||||
total={view.list?.listItemCount}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.align_end,
|
||||
a.gap_lg,
|
||||
web({
|
||||
position: 'static',
|
||||
zIndex: 'unset',
|
||||
}),
|
||||
]}>
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_md, a.font_semi_bold, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
{record.name}
|
||||
</Text>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_xs, t.atoms.text_contrast_medium, a.leading_snug]}
|
||||
numberOfLines={2}>
|
||||
{record.description}
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Follow all`)}
|
||||
disabled={isProcessing || isFollowingAll}
|
||||
onPress={onFollowAll}
|
||||
color="secondary"
|
||||
size="small"
|
||||
style={[a.z_50]}>
|
||||
<ButtonText>
|
||||
<Trans>Follow all</Trans>
|
||||
</ButtonText>
|
||||
{isFollowingAll ? (
|
||||
<ButtonIcon icon={CheckIcon} />
|
||||
) : (
|
||||
isProcessing && <ButtonIcon icon={Loader} />
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import {useContext} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useOnboardingSuggestedStarterPacksQuery} from '#/state/queries/useOnboardingSuggestedStarterPacksQuery'
|
||||
import {OnboardingControls} from '#/screens/Onboarding/Layout'
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {StarterPackCard} from './StarterPackCard'
|
||||
|
||||
export function StepSuggestedStarterpacks() {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
const {state, dispatch} = useContext(Context)
|
||||
|
||||
const {
|
||||
data: suggestedStarterPacks,
|
||||
isLoading,
|
||||
isError,
|
||||
isRefetching,
|
||||
refetch,
|
||||
} = useOnboardingSuggestedStarterPacksQuery({
|
||||
enabled: true,
|
||||
overrideInterests: state.interestsStepResults.selectedInterests,
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={[a.align_start]} testID="onboardingInterests">
|
||||
<Text style={[a.font_bold, a.text_3xl]}>
|
||||
<Trans comment="Accounts suggested to the user for them to follow">
|
||||
Suggested for you
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.overflow_hidden,
|
||||
a.mt_lg,
|
||||
a.flex_1,
|
||||
a.justify_start,
|
||||
a.w_full,
|
||||
]}>
|
||||
{isLoading || !moderationOpts ? (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.mt_md,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{minHeight: 400},
|
||||
]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
) : isError ? (
|
||||
<View style={[a.flex_1, a.px_xl, a.pt_5xl]}>
|
||||
<Admonition type="error">
|
||||
<Trans>
|
||||
An error occurred while fetching suggested accounts.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[a.flex_1, a.mt_md]}>
|
||||
{suggestedStarterPacks?.starterPacks.map(starterPack => (
|
||||
<View style={[a.pb_lg]} key={starterPack.uri}>
|
||||
<StarterPackCard view={starterPack} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
{isError ? (
|
||||
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
||||
<Button
|
||||
disabled={isRefetching}
|
||||
color="secondary"
|
||||
size="large"
|
||||
label={_(msg`Retry`)}
|
||||
onPress={() => refetch()}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={ArrowRotateCounterClockwiseIcon} />
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="large"
|
||||
label={_(msg`Skip to next step`)}
|
||||
onPress={() => dispatch({type: 'next'})}>
|
||||
<ButtonText>
|
||||
<Trans>Skip</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
||||
<Button
|
||||
color="primary"
|
||||
size="large"
|
||||
label={_(msg`Continue to next step`)}
|
||||
onPress={() => dispatch({type: 'next'})}>
|
||||
<ButtonText>
|
||||
<Trans>Continue</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</OnboardingControls.Portal>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import {useMemo, useReducer} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import * as bcp47Match from 'bcp-47-match'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {
|
||||
Layout,
|
||||
OnboardingControls,
|
||||
@@ -12,19 +15,34 @@ import {StepFinished} from '#/screens/Onboarding/StepFinished'
|
||||
import {StepInterests} from '#/screens/Onboarding/StepInterests'
|
||||
import {StepProfile} from '#/screens/Onboarding/StepProfile'
|
||||
import {Portal} from '#/components/Portal'
|
||||
import {ScreenTransition} from '#/components/ScreenTransition'
|
||||
import {ENV} from '#/env'
|
||||
import {StepSuggestedAccounts} from './StepSuggestedAccounts'
|
||||
import {StepSuggestedStarterpacks} from './StepSuggestedStarterpacks'
|
||||
|
||||
export function Onboarding() {
|
||||
const {_} = useLingui()
|
||||
const gate = useGate()
|
||||
|
||||
const {contentLanguages} = useLanguagePrefs()
|
||||
const probablySpeaksEnglish = useMemo(() => {
|
||||
if (contentLanguages.length === 0) return true
|
||||
return bcp47Match.basicFilter('en', contentLanguages).length > 0
|
||||
}, [contentLanguages])
|
||||
|
||||
// starter packs screen is currently geared towards english-speaking accounts
|
||||
const showSuggestedStarterpacks =
|
||||
ENV !== 'e2e' &&
|
||||
probablySpeaksEnglish &&
|
||||
gate('onboarding_suggested_starterpacks')
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
...initialState,
|
||||
totalSteps: 4,
|
||||
totalSteps: 4 + (showSuggestedStarterpacks ? 1 : 0),
|
||||
experiments: {
|
||||
// let's leave this flag logic in for now to avoid rebase churn
|
||||
// TODO: remove this flag logic once we've finished with all experiments -sfn
|
||||
onboarding_suggested_accounts: true,
|
||||
onboarding_value_prop: true,
|
||||
onboarding_suggested_starterpacks: showSuggestedStarterpacks,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -65,12 +83,19 @@ export function Onboarding() {
|
||||
[state, dispatch, interestsDisplayNames],
|
||||
)}>
|
||||
<Layout>
|
||||
{state.activeStep === 'profile' && <StepProfile />}
|
||||
{state.activeStep === 'interests' && <StepInterests />}
|
||||
{state.activeStep === 'suggested-accounts' && (
|
||||
<StepSuggestedAccounts />
|
||||
)}
|
||||
{state.activeStep === 'finished' && <StepFinished />}
|
||||
<ScreenTransition
|
||||
key={state.activeStep}
|
||||
direction={state.stepTransitionDirection}>
|
||||
{state.activeStep === 'profile' && <StepProfile />}
|
||||
{state.activeStep === 'interests' && <StepInterests />}
|
||||
{state.activeStep === 'suggested-accounts' && (
|
||||
<StepSuggestedAccounts />
|
||||
)}
|
||||
{state.activeStep === 'suggested-starterpacks' && (
|
||||
<StepSuggestedStarterpacks />
|
||||
)}
|
||||
{state.activeStep === 'finished' && <StepFinished />}
|
||||
</ScreenTransition>
|
||||
</Layout>
|
||||
</Context.Provider>
|
||||
</OnboardingHeaderSlot.Provider>
|
||||
|
||||
+34
-106
@@ -1,6 +1,4 @@
|
||||
import React from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {
|
||||
@@ -11,12 +9,17 @@ import {
|
||||
export type OnboardingState = {
|
||||
hasPrev: boolean
|
||||
totalSteps: number
|
||||
activeStep: 'profile' | 'interests' | 'suggested-accounts' | 'finished'
|
||||
activeStep:
|
||||
| 'profile'
|
||||
| 'interests'
|
||||
| 'suggested-accounts'
|
||||
| 'suggested-starterpacks'
|
||||
| 'finished'
|
||||
activeStepIndex: number
|
||||
stepTransitionDirection: 'Forward' | 'Backward'
|
||||
|
||||
interestsStepResults: {
|
||||
selectedInterests: string[]
|
||||
apiResponse: ApiResponseMap
|
||||
}
|
||||
profileStepResults: {
|
||||
isCreatedAvatar: boolean
|
||||
@@ -38,6 +41,7 @@ export type OnboardingState = {
|
||||
experiments?: {
|
||||
onboarding_suggested_accounts?: boolean
|
||||
onboarding_value_prop?: boolean
|
||||
onboarding_suggested_starterpacks?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +58,6 @@ export type OnboardingAction =
|
||||
| {
|
||||
type: 'setInterestsStepResults'
|
||||
selectedInterests: string[]
|
||||
apiResponse: ApiResponseMap
|
||||
}
|
||||
| {
|
||||
type: 'setProfileStepResults'
|
||||
@@ -70,75 +73,15 @@ export type OnboardingAction =
|
||||
| undefined
|
||||
}
|
||||
|
||||
export type ApiResponseMap = {
|
||||
interests: string[]
|
||||
suggestedAccountDids: {
|
||||
[key: string]: string[]
|
||||
}
|
||||
suggestedFeedUris: {
|
||||
[key: string]: string[]
|
||||
}
|
||||
}
|
||||
|
||||
// most popular selected interests
|
||||
export const popularInterests = [
|
||||
'art',
|
||||
'gaming',
|
||||
'sports',
|
||||
'comics',
|
||||
'music',
|
||||
'politics',
|
||||
'photography',
|
||||
'science',
|
||||
'news',
|
||||
]
|
||||
|
||||
export function useInterestsDisplayNames() {
|
||||
const {_} = useLingui()
|
||||
|
||||
return React.useMemo<Record<string, string>>(() => {
|
||||
return {
|
||||
// Keep this alphabetized
|
||||
animals: _(msg`Animals`),
|
||||
art: _(msg`Art`),
|
||||
books: _(msg`Books`),
|
||||
comedy: _(msg`Comedy`),
|
||||
comics: _(msg`Comics`),
|
||||
culture: _(msg`Culture`),
|
||||
dev: _(msg`Software Dev`),
|
||||
education: _(msg`Education`),
|
||||
food: _(msg`Food`),
|
||||
gaming: _(msg`Video Games`),
|
||||
journalism: _(msg`Journalism`),
|
||||
movies: _(msg`Movies`),
|
||||
music: _(msg`Music`),
|
||||
nature: _(msg`Nature`),
|
||||
news: _(msg`News`),
|
||||
pets: _(msg`Pets`),
|
||||
photography: _(msg`Photography`),
|
||||
politics: _(msg`Politics`),
|
||||
science: _(msg`Science`),
|
||||
sports: _(msg`Sports`),
|
||||
tech: _(msg`Tech`),
|
||||
tv: _(msg`TV`),
|
||||
writers: _(msg`Writers`),
|
||||
}
|
||||
}, [_])
|
||||
}
|
||||
|
||||
export const initialState: OnboardingState = {
|
||||
hasPrev: false,
|
||||
totalSteps: 3,
|
||||
activeStep: 'profile',
|
||||
activeStepIndex: 1,
|
||||
stepTransitionDirection: 'Forward',
|
||||
|
||||
interestsStepResults: {
|
||||
selectedInterests: [],
|
||||
apiResponse: {
|
||||
interests: [],
|
||||
suggestedAccountDids: {},
|
||||
suggestedFeedUris: {},
|
||||
},
|
||||
},
|
||||
profileStepResults: {
|
||||
isCreatedAvatar: false,
|
||||
@@ -163,52 +106,38 @@ export function reducer(
|
||||
): OnboardingState {
|
||||
let next = {...s}
|
||||
|
||||
const stepOrder: OnboardingState['activeStep'][] = [
|
||||
'profile',
|
||||
'interests',
|
||||
...(s.experiments?.onboarding_suggested_accounts
|
||||
? (['suggested-accounts'] as const)
|
||||
: []),
|
||||
...(s.experiments?.onboarding_suggested_starterpacks
|
||||
? (['suggested-starterpacks'] as const)
|
||||
: []),
|
||||
'finished',
|
||||
]
|
||||
|
||||
switch (a.type) {
|
||||
case 'next': {
|
||||
if (s.experiments?.onboarding_suggested_accounts) {
|
||||
if (s.activeStep === 'profile') {
|
||||
next.activeStep = 'interests'
|
||||
next.activeStepIndex = 2
|
||||
} else if (s.activeStep === 'interests') {
|
||||
next.activeStep = 'suggested-accounts'
|
||||
next.activeStepIndex = 3
|
||||
}
|
||||
if (s.activeStep === 'suggested-accounts') {
|
||||
next.activeStep = 'finished'
|
||||
next.activeStepIndex = 4
|
||||
}
|
||||
} else {
|
||||
if (s.activeStep === 'profile') {
|
||||
next.activeStep = 'interests'
|
||||
next.activeStepIndex = 2
|
||||
} else if (s.activeStep === 'interests') {
|
||||
next.activeStep = 'finished'
|
||||
next.activeStepIndex = 3
|
||||
}
|
||||
// 1-indexed for some reason
|
||||
const nextIndex = s.activeStepIndex
|
||||
const nextStep = stepOrder[nextIndex]
|
||||
if (nextStep) {
|
||||
next.activeStep = nextStep
|
||||
next.activeStepIndex = nextIndex + 1
|
||||
}
|
||||
next.stepTransitionDirection = 'Forward'
|
||||
break
|
||||
}
|
||||
case 'prev': {
|
||||
if (s.experiments?.onboarding_suggested_accounts) {
|
||||
if (s.activeStep === 'interests') {
|
||||
next.activeStep = 'profile'
|
||||
next.activeStepIndex = 1
|
||||
} else if (s.activeStep === 'suggested-accounts') {
|
||||
next.activeStep = 'interests'
|
||||
next.activeStepIndex = 2
|
||||
} else if (s.activeStep === 'finished') {
|
||||
next.activeStep = 'suggested-accounts'
|
||||
next.activeStepIndex = 3
|
||||
}
|
||||
} else {
|
||||
if (s.activeStep === 'interests') {
|
||||
next.activeStep = 'profile'
|
||||
next.activeStepIndex = 1
|
||||
} else if (s.activeStep === 'finished') {
|
||||
next.activeStep = 'interests'
|
||||
next.activeStepIndex = 2
|
||||
}
|
||||
const prevIndex = s.activeStepIndex - 2
|
||||
const prevStep = stepOrder[prevIndex]
|
||||
if (prevStep) {
|
||||
next.activeStep = prevStep
|
||||
next.activeStepIndex = prevIndex + 1
|
||||
}
|
||||
next.stepTransitionDirection = 'Backward'
|
||||
break
|
||||
}
|
||||
case 'finish': {
|
||||
@@ -218,7 +147,6 @@ export function reducer(
|
||||
case 'setInterestsStepResults': {
|
||||
next.interestsStepResults = {
|
||||
selectedInterests: a.selectedInterests,
|
||||
apiResponse: a.apiResponse,
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {usePostThreadQuery} from '#/state/queries/post-thread'
|
||||
import {usePostQuery} from '#/state/queries/post'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
|
||||
import * as Layout from '#/components/Layout'
|
||||
@@ -17,11 +17,11 @@ export const PostLikedByScreen = ({route}: Props) => {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const {data: post} = usePostThreadQuery(uri)
|
||||
const {data: post} = usePostQuery(uri)
|
||||
|
||||
let likeCount
|
||||
if (post?.thread.type === 'post') {
|
||||
likeCount = post.thread.post.likeCount
|
||||
if (post) {
|
||||
likeCount = post.likeCount
|
||||
}
|
||||
|
||||
useFocusEffect(
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {usePostThreadQuery} from '#/state/queries/post-thread'
|
||||
import {usePostQuery} from '#/state/queries/post'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
|
||||
import * as Layout from '#/components/Layout'
|
||||
@@ -17,11 +17,11 @@ export const PostQuotesScreen = ({route}: Props) => {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const {data: post} = usePostThreadQuery(uri)
|
||||
const {data: post} = usePostQuery(uri)
|
||||
|
||||
let quoteCount
|
||||
if (post?.thread.type === 'post') {
|
||||
quoteCount = post.thread.post.quoteCount
|
||||
if (post) {
|
||||
quoteCount = post.quoteCount
|
||||
}
|
||||
|
||||
useFocusEffect(
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {usePostThreadQuery} from '#/state/queries/post-thread'
|
||||
import {usePostQuery} from '#/state/queries/post'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy'
|
||||
import * as Layout from '#/components/Layout'
|
||||
@@ -17,11 +17,11 @@ export const PostRepostedByScreen = ({route}: Props) => {
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {data: post} = usePostThreadQuery(uri)
|
||||
const {data: post} = usePostQuery(uri)
|
||||
|
||||
let quoteCount
|
||||
if (post?.thread.type === 'post') {
|
||||
quoteCount = post.thread.post.repostCount
|
||||
if (post) {
|
||||
quoteCount = post.repostCount
|
||||
}
|
||||
|
||||
useFocusEffect(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user