Compare commits

...

46 Commits

Author SHA1 Message Date
Eric Bailey df4c03ee07 Disable Sentry sampling for now 2025-10-22 13:50:04 -05:00
Samuel Newman ea00663e7f Android CI - Clear cache on second build (#9223)
* clear cache on second build

* alternative cache cleaning method
2025-10-22 10:05:00 -07:00
Samuel Newman 12e57378e8 fix android testflight ci (#9237) 2025-10-22 10:04:36 -07:00
Eric Bailey 24606dbcfc Don't need to be logging upstream errors (#9260) 2025-10-22 11:39:14 -05:00
Eric Bailey 6f31121e51 Check for network error before reporting push token issue (#9258) 2025-10-22 11:36:40 -05:00
Eric Bailey 1abc96314b Don't log network errors (#9257) 2025-10-22 11:36:32 -05:00
Eric Bailey 7cc897d096 Remove error log for Bluesky feed service issues (#9256) 2025-10-22 11:36:24 -05:00
Eric Bailey 6ed30213c8 Add 'Upstream service unreachable' to network errors (#9255) 2025-10-22 11:36:18 -05:00
Eric Bailey d7e8429753 Get iOS debug files into Sentry (#9242)
* Add buildArtifactPaths to iOS builds

* Update privacyManifests

* Revert changes to build configs

* Add --include-sources flag

* Add nativeCrash tester

* Sam's crash testbed

(cherry picked from commit b0a0cb7799cd900777570cae7dc10461cf811e0f)

* Output artifacts, tarball, extract and upload dSYM to Sentry

* Revert "Sam's crash testbed"

This reverts commit cc2c9828a0.

* Remove sources for other targets, include sources for main bundle

* Apply suggestion from @mozzius

* Apply suggestion from @mozzius

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2025-10-22 09:17:52 -05:00
Alex Benzer 6086bc5d5c Add a client event for "See more" link on suggested user interstitials (#9249) 2025-10-22 03:53:42 -07:00
Samuel Newman 199821d670 make gifs square again (#9236) 2025-10-20 07:15:01 -07:00
Eric Bailey 11dff765db Hook up post shadow to saved posts (#9225) 2025-10-20 03:05:49 -07:00
hailey 73639aa571 fix regex (#9234) 2025-10-19 09:20:53 -07:00
pfrazee f3b6d85913 Nightly source-language update 2025-10-17 02:36:59 +00:00
Samuel Newman 239d3cc535 [Release prep] 1.109.0 translations (#9218)
* intl:pull

* intl:extract:all
2025-10-16 10:08:15 -07:00
Samuel Newman d49d43f6eb update app icon (#9219) 2025-10-16 10:08:06 -07:00
Samuel Newman a482dae936 [Release prep] 1.109.0 E2E fixes (#9216)
* list tests

* fix create account

* get onboarding e2e tests working

* get curate list test working

* Update thread-muting.yml
2025-10-16 07:31:11 -07:00
Samuel Newman 25d44d8939 update xcode (#9215) 2025-10-16 07:26:52 -07:00
pfrazee e7801a0b76 Nightly source-language update 2025-10-16 02:37:46 +00:00
Eric Bailey 7861cd153d Remove notice in create account flow (#9209)
* Remove notice of policy updates

* Rm debug code
2025-10-15 13:56:29 -05:00
Samuel Newman be53a168ba Move interests clientside and add finance (#9176)
* move interests clientside

* add finance

* try and ensure there's always data for the "all" category

* add empty state for suggested accounts

* simplify error wording
2025-10-15 09:37:24 -07:00
Samuel Newman b56d0c419b add sort import suggestions plugin (#8335) 2025-10-15 09:37:07 -07:00
Samuel Newman 03adb4260f patch fractional font size issue (#9199) 2025-10-15 09:14:42 -07:00
Eric Bailey 27926f093c Fix computation of isLastSibling, isLastChild, and replyIndex (#9202)
* Fix computation of isLastSibling and isLastChild to account for muted or
otherwise hidden replies

* Update comments

* isLastSiblingByCounts isn't needed, should rely only on the count of replies seen

* The counters serve the same purpose, we only need to know the count of the actual replies rendered to the view in order to calculate the replyIndex

* Remove redundant check
2025-10-15 11:13:08 -05:00
Samuel Newman fe5a623b44 Haptic on profile buttons (#6034)
* play haptic on profile buttons

* change react import
2025-10-15 09:10:26 -07:00
Samuel Newman fc944841ce add large content viewer to tabs (#8394) 2025-10-15 11:02:47 -05:00
Samuel Newman 66ca3e34a4 fix invalid tweaking to date string (#8415) 2025-10-15 11:00:29 -05:00
Samuel Newman 4788106f32 change flatlist config for explore page (#9083) 2025-10-15 08:57:30 -07:00
Samuel Newman 6414bda252 delay toast after posting (#9208) 2025-10-15 08:56:35 -07:00
Samuel Newman 591fd262b3 make sure border radius matches inset border (#9207) 2025-10-15 08:55:46 -07:00
Samuel Newman ede10368d5 cache pages in explore feed previews (#9082) 2025-10-15 18:54:54 +03:00
Samuel Newman 1127e50248 [Onboarding] Add starterpacks step (#9007)
* add onboarding starterpacks step

* tweak starter pack cards

* fix web styling

* use new API

* add screen transitions to onboarding

* skip host account

* bump limit to 6

* tweak font weight

* rm commented out code

* fix yarn.lock

* limit starter packs experiment to english speakers
2025-10-15 07:32:38 -07:00
jim e4485da4ff Merge pull request #9189 from internet-development/binaryfiddler/hashtag
prompt user to log in on hashtag screen
2025-10-15 01:12:49 -07:00
Alex Benzer 6c7fd96528 Add metrics for when OPs click "hide reply for me" or "hide reply for everyone" (#9206) 2025-10-15 08:33:30 +01:00
pfrazee 6a1c0ff346 Nightly source-language update 2025-10-15 02:38:20 +00:00
Chenyu Huang ad18439148 use hasSession 2025-10-14 14:15:19 -07:00
Eric Bailey 7280e44ea2 Remove log (#9204) 2025-10-14 11:49:02 -05:00
Eric Bailey 7e6b23bd86 Fix ternary in moderation settings that caused a double title (#9198) 2025-10-14 09:17:34 -05:00
pfrazee d2bc22e9be Nightly source-language update 2025-10-14 02:36:22 +00:00
Eric Bailey 0b50e9f109 Remove remaining usages of old post thread query (#9184)
* Remove remaining usages of old post thread query

* Add PostThreadContext, cache mutator for threadgates on threads, pipe it through

* Replace getPostThread in threadgate query

* Replace in initQuote handling, which isn't even used rn...

* Missing import

* Revert ext change
2025-10-13 18:36:11 -05:00
Samuel Newman 0f6a99c8a5 enable numberOfLines on replyTo component (#9196) 2025-10-13 09:36:06 -07:00
Samuel Newman ea9986f127 fix label spacing (#9195) 2025-10-13 09:35:31 -07:00
Samuel Newman 5222a6e1c3 tidy up post reasons (#9194) 2025-10-13 08:24:02 -07:00
pfrazee b7f505ab00 Nightly source-language update 2025-10-11 02:32:55 +00:00
Chenyu Huang 34f2cc2322 prompt user to log in on hashtag screen 2025-10-10 14:17:57 -07:00
Eric Bailey 8a30131686 Combine SubtleWebHover and SubtleHover, default to web only for all cases (#9188) 2025-10-10 15:05:20 -05:00
144 changed files with 68328 additions and 68071 deletions
+10 -5
View File
@@ -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
+23 -4
View File
@@ -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
+6 -3
View File
@@ -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"
+19 -33
View File
@@ -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"
+2 -6
View File
@@ -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"
+8 -1
View File
@@ -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:
+10 -2
View File
@@ -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:
+4
View File
@@ -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"
+6 -6
View File
@@ -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
View File
@@ -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

+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+8 -4
View File
@@ -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
View File
@@ -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))
}
+5 -1
View File
@@ -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}}>
+11 -1
View File
@@ -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,
+2 -2
View File
@@ -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}}
+3 -1
View File
@@ -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,
+18 -3
View File
@@ -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
}
-5
View File
@@ -1,5 +0,0 @@
import {type ViewStyleProp} from '#/alf'
export function SubtleWebHover({}: ViewStyleProp & {hover: boolean}) {
return null
}
-49
View File
@@ -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',
},
})
+1 -1
View File
@@ -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 && (
+1 -4
View File
@@ -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],
)
+1
View File
@@ -78,6 +78,7 @@ const NETWORK_ERRORS = [
'Network request failed',
'Failed to fetch',
'Load failed',
'Upstream service unreachable',
]
export function isNetworkError(e: unknown) {
+78
View File
@@ -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 ? '' : '@')
}
+4 -1
View File
@@ -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})
}
}
}
+1 -1
View File
@@ -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'
+1
View File
@@ -31,6 +31,7 @@ const NETWORK_ERRORS = [
'Network request failed',
'Failed to fetch',
'Load failed',
'Upstream service unreachable',
]
export function isNetworkError(e: unknown) {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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 ? (
+16 -27
View File
@@ -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()
+42 -206
View File
@@ -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>
)
}
+34 -9
View File
@@ -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
View File
@@ -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
}
+4 -4
View File
@@ -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(
+4 -4
View File
@@ -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(
+4 -4
View File
@@ -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