Merge remote-tracking branch 'origin/main' into next/base

* origin/main:
  Make Android app start faster by disabling JS bundle compression (#7751)
  Nightly source-language update
  Update tests
  Screen for searching user's posts (#7622)
  Add translations missed in last PR (#7748)
  1.98 release: Pull latest from crowdin (#7746)
  [Instrumentation] Signin (#7742)
  Reenable router events (#7735)
  Nightly source-language update
  Bitdrift integration (#7728)
  Use effective filtering for feeds (#7736)
  Update PostInteractionSettingsDialog.tsx (#7726)
This commit is contained in:
Eric Bailey
2025-02-17 10:25:31 -06:00
60 changed files with 43862 additions and 59163 deletions
+1 -1
View File
@@ -160,7 +160,7 @@ appId: xyz.blueskyweb.app
- tapOn: - tapOn:
label: "Adds and removes users on curatelists from the profile" label: "Adds and removes users on curatelists from the profile"
id: "bottomBarSearchBtn" id: "bottomBarSearchBtn"
- tapOn: "Search" - tapOn: "Search for posts, users, or feeds"
- inputText: "bob" - inputText: "bob"
- tapOn: - tapOn:
id: "searchAutoCompleteResult-bob.test" id: "searchAutoCompleteResult-bob.test"
+1 -1
View File
@@ -16,7 +16,7 @@ appId: xyz.blueskyweb.app
- tapOn: - tapOn:
id: "profileCardButton" id: "profileCardButton"
- tapOn: - tapOn:
id: "profilePager-selector-4" id: "profilePager-selector-5"
- tapOn: "alice-favs" - tapOn: "alice-favs"
- tapOn: "Pin to Home" - tapOn: "Pin to Home"
- tapOn: - tapOn:
+1 -1
View File
@@ -15,7 +15,7 @@ appId: xyz.blueskyweb.app
id: "bottomBarSearchBtn" id: "bottomBarSearchBtn"
- tapOn: - tapOn:
id: "bottomBarSearchBtn" id: "bottomBarSearchBtn"
- tapOn: "Search" - tapOn: "Search for posts, users, or feeds"
- inputText: "b" - inputText: "b"
- tapOn: - tapOn:
id: "searchAutoCompleteResult-bob.test" id: "searchAutoCompleteResult-bob.test"
+2 -2
View File
@@ -12,8 +12,8 @@ appId: xyz.blueskyweb.app
# Navigate to another user profile via autocomplete # Navigate to another user profile via autocomplete
- tapOn: - tapOn:
id: "bottomBarSearchBtn" id: "bottomBarSearchBtn"
- assertVisible: "Search" - assertVisible: "Search for posts, users, or feeds"
- tapOn: "Search" - tapOn: "Search for posts, users, or feeds"
- inputText: "b" - inputText: "b"
- tapOn: - tapOn:
id: "searchAutoCompleteResult-bob.test" id: "searchAutoCompleteResult-bob.test"
+1
View File
@@ -242,6 +242,7 @@ module.exports = function (config) {
'./plugins/withAndroidStylesAccentColorPlugin.js', './plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js', './plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
'./plugins/withAndroidNoJitpackPlugin.js', './plugins/withAndroidNoJitpackPlugin.js',
'./plugins/withNoBundleCompression.js',
'./plugins/shareExtension/withShareExtensions.js', './plugins/shareExtension/withShareExtensions.js',
'./plugins/notificationsExtension/withNotificationsExtension.js', './plugins/notificationsExtension/withNotificationsExtension.js',
'./plugins/withAppDelegateReferrer.js', './plugins/withAppDelegateReferrer.js',
+1
View File
@@ -283,6 +283,7 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handleOrDID/follows", server.WebGeneric) e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
e.GET("/profile/:handleOrDID/followers", server.WebGeneric) e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric) e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
e.GET("/profile/:handleOrDID/search", server.WebGeneric)
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric) e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric) e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric) e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
+1 -1
View File
@@ -57,7 +57,7 @@
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.14.0", "@atproto/api": "^0.14.0",
"@bitdrift/react-native": "^0.6.2", "@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
+70
View File
@@ -0,0 +1,70 @@
const {withAppBuildGradle} = require('@expo/config-plugins')
/**
* A Config Plugin to disable bundle compression in Android build.gradle.
* @param {import('@expo/config-plugins').ConfigPlugin} config
* @returns {import('@expo/config-plugins').ConfigPlugin}
*/
module.exports = function withNoBundleCompression(config) {
return withAppBuildGradle(config, androidConfig => {
let buildGradle = androidConfig.modResults.contents
const hasAndroidResources = buildGradle.includes('androidResources {')
const hasNoCompress = buildGradle.includes('noCompress')
if (hasAndroidResources) {
if (hasNoCompress) {
if (
buildGradle.includes('noCompress += ["bundle"]') ||
buildGradle.includes("noCompress += 'bundle'") ||
buildGradle.includes('noCompress += "bundle"')
) {
return androidConfig
}
const lines = buildGradle.split('\n')
const modifiedLines = lines.map(line => {
if (line.trim().startsWith('noCompress')) {
if (line.includes('+=')) {
return line.replace(/\]/, ', "bundle"]')
} else if (line.includes('=')) {
return line.replace('=', '+= ["bundle",') + ']'
}
}
return line
})
androidConfig.modResults.contents = modifiedLines.join('\n')
} else {
const androidResources = buildGradle.indexOf('androidResources {')
if (androidResources === -1) {
throw new Error(
`Cannot find androidResources { block in build.gradle!`,
)
}
const insertPosition = buildGradle.indexOf('\n', androidResources) + 1
const newContent =
buildGradle.slice(0, insertPosition) +
' noCompress += ["bundle"]\n' +
buildGradle.slice(insertPosition)
androidConfig.modResults.contents = newContent
}
} else {
const androidBlock = buildGradle.indexOf('android {')
if (androidBlock === -1) {
throw new Error(`Cannot find android { block in build.gradle!`)
}
const insertPosition = buildGradle.indexOf('\n', androidBlock) + 1
const newContent =
buildGradle.slice(0, insertPosition) +
' androidResources {\n' +
' noCompress += ["bundle"]\n' +
' }\n' +
buildGradle.slice(insertPosition)
androidConfig.modResults.contents = newContent
}
return androidConfig
})
}
+13 -4
View File
@@ -91,6 +91,7 @@ import {VideoFeed} from '#/screens/VideoFeed'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import {router} from '#/routes' import {router} from '#/routes'
import {Referrer} from '../modules/expo-bluesky-swiss-army' import {Referrer} from '../modules/expo-bluesky-swiss-army'
import {ProfileSearchScreen} from './screens/Profile/ProfileSearch'
import {AboutSettingsScreen} from './screens/Settings/AboutSettings' import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings'
import {AccountSettingsScreen} from './screens/Settings/AccountSettings' import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
@@ -207,6 +208,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => ProfileListScreen} getComponent={() => ProfileListScreen}
options={{title: title(msg`List`), requireAuth: true}} options={{title: title(msg`List`), requireAuth: true}}
/> />
<Stack.Screen
name="ProfileSearch"
getComponent={() => ProfileSearchScreen}
options={({route}) => ({
title: title(msg`Search @${route.params.name}'s posts`),
})}
/>
<Stack.Screen <Stack.Screen
name="PostThread" name="PostThread"
getComponent={() => PostThreadScreen} getComponent={() => PostThreadScreen}
@@ -721,15 +729,16 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
linking={LINKING} linking={LINKING}
theme={theme} theme={theme}
onStateChange={() => { onStateChange={() => {
const routeName = getCurrentRouteName() logEvent('lake:router:navigate', {
if (routeName === 'Notifications') { from: prevLoggedRouteName.current,
logEvent('router:navigate:notifications', {}) })
} prevLoggedRouteName.current = getCurrentRouteName()
}} }}
onReady={() => { onReady={() => {
attachRouteToLogEvents(getCurrentRouteName) attachRouteToLogEvents(getCurrentRouteName)
logModuleInitTime() logModuleInitTime()
onReady() onReady()
logEvent('lake:router:navigate', {})
}}> }}>
{children} {children}
</NavigationContainer> </NavigationContainer>
@@ -81,8 +81,9 @@ export function PostInteractionSettingsControlledDialog({
<Trans> <Trans>
You can set default interaction settings in{' '} You can set default interaction settings in{' '}
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}> <Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
Settings &rarr; Moderation &rarr; Interaction settings. Settings &rarr; Moderation &rarr; Interaction settings
</Text> </Text>
.
</Trans> </Trans>
</Text> </Text>
</View> </View>
+9 -2
View File
@@ -1,23 +1,30 @@
import {init, SessionStrategy} from '@bitdrift/react-native' import {init, SessionStrategy} from '@bitdrift/react-native'
import {Statsig} from 'statsig-react-native-expo' import {Statsig} from 'statsig-react-native-expo'
export {debug, error, info, warn} from '@bitdrift/react-native'
import {initPromise} from './statsig/statsig' import {initPromise} from './statsig/statsig'
export {debug, error, info, warn} from '@bitdrift/react-native'
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
initPromise.then(() => { initPromise.then(() => {
let isEnabled = false let isEnabled = false
let isNetworkEnabled = false
try { try {
if (Statsig.checkGate('enable_bitdrift')) { if (Statsig.checkGate('enable_bitdrift_v2')) {
isEnabled = true isEnabled = true
} }
if (Statsig.checkGate('enable_bitdrift_v2_networking')) {
isNetworkEnabled = true
}
} catch (e) { } catch (e) {
// Statsig may complain about it being called too early. // Statsig may complain about it being called too early.
} }
if (isEnabled && BITDRIFT_API_KEY) { if (isEnabled && BITDRIFT_API_KEY) {
init(BITDRIFT_API_KEY, SessionStrategy.Activity, { init(BITDRIFT_API_KEY, SessionStrategy.Activity, {
url: 'https://api-bsky.bitdrift.io', url: 'https://api-bsky.bitdrift.io',
// Only effects iOS, Android instrumentation is set via Gradle Plugin
enableNetworkInstrumentation: isNetworkEnabled,
}) })
} }
}) })
+1
View File
@@ -18,6 +18,7 @@ export type CommonNavigatorParams = {
ProfileFollowers: {name: string} ProfileFollowers: {name: string}
ProfileFollows: {name: string} ProfileFollows: {name: string}
ProfileKnownFollowers: {name: string} ProfileKnownFollowers: {name: string}
ProfileSearch: {name: string; q?: string}
ProfileList: {name: string; rkey: string} ProfileList: {name: string; rkey: string}
PostThread: {name: string; rkey: string} PostThread: {name: string; rkey: string}
PostLikedBy: {name: string; rkey: string} PostLikedBy: {name: string; rkey: string}
+19 -1
View File
@@ -30,7 +30,9 @@ export type LogEvents = {
secondsActive: number secondsActive: number
} }
'state:foreground': {} 'state:foreground': {}
'router:navigate:notifications': {} 'lake:router:navigate': {
from?: string
}
'deepLink:referrerReceived': { 'deepLink:referrerReceived': {
to: string to: string
referrer: string referrer: string
@@ -49,6 +51,22 @@ export type LogEvents = {
} }
'signup:captchaSuccess': {} 'signup:captchaSuccess': {}
'signup:captchaFailure': {} 'signup:captchaFailure': {}
'signin:hostingProviderPressed': {
hostingProviderDidChange: boolean
}
'signin:hostingProviderFailedResolution': {}
'signin:success': {
failedAttemptsCount: number
isUsingCustomProvider: boolean
timeTakenSeconds: number
}
'signin:backPressed': {
failedAttemptsCount: number
}
'signin:forgotPasswordPressed': {}
'signin:passwordReset': {}
'signin:passwordResetSuccess': {}
'signin:passwordResetFailure': {}
'onboarding:interests:nextPressed': { 'onboarding:interests:nextPressed': {
selectedInterests: string[] selectedInterests: string[]
selectedInterestsLength: number selectedInterestsLength: number
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
+1
View File
@@ -19,6 +19,7 @@ export const router = new Router({
ProfileFollowers: '/profile/:name/followers', ProfileFollowers: '/profile/:name/followers',
ProfileFollows: '/profile/:name/follows', ProfileFollows: '/profile/:name/follows',
ProfileKnownFollowers: '/profile/:name/known-followers', ProfileKnownFollowers: '/profile/:name/known-followers',
ProfileSearch: '/profile/:name/search',
ProfileList: '/profile/:name/lists/:rkey', ProfileList: '/profile/:name/lists/:rkey',
PostThread: '/profile/:name/post/:rkey', PostThread: '/profile/:name/post/:rkey',
PostLikedBy: '/profile/:name/post/:rkey/liked-by', PostLikedBy: '/profile/:name/post/:rkey/liked-by',
+30 -22
View File
@@ -45,6 +45,8 @@ export const LoginForm = ({
onPressRetryConnect, onPressRetryConnect,
onPressBack, onPressBack,
onPressForgotPassword, onPressForgotPassword,
onAttemptSuccess,
onAttemptFailed,
}: { }: {
error: string error: string
serviceUrl: string serviceUrl: string
@@ -55,6 +57,8 @@ export const LoginForm = ({
onPressRetryConnect: () => void onPressRetryConnect: () => void
onPressBack: () => void onPressBack: () => void
onPressForgotPassword: () => void onPressForgotPassword: () => void
onAttemptSuccess: () => void
onAttemptFailed: () => void
}) => { }) => {
const t = useTheme() const t = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false) const [isProcessing, setIsProcessing] = useState<boolean>(false)
@@ -131,6 +135,7 @@ export const LoginForm = ({
}, },
'LoginForm', 'LoginForm',
) )
onAttemptSuccess()
setShowLoggedOut(false) setShowLoggedOut(false)
setHasCheckedForStarterPack(true) setHasCheckedForStarterPack(true)
requestNotificationsPermission('Login') requestNotificationsPermission('Login')
@@ -142,29 +147,32 @@ export const LoginForm = ({
e instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError e instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError
) { ) {
setIsAuthFactorTokenNeeded(true) setIsAuthFactorTokenNeeded(true)
} else if (errMsg.includes('Token is invalid')) {
logger.debug('Failed to login due to invalid 2fa token', {
error: errMsg,
})
setError(_(msg`Invalid 2FA confirmation code.`))
} else if (
errMsg.includes('Authentication Required') ||
errMsg.includes('Invalid identifier or password')
) {
logger.debug('Failed to login due to invalid credentials', {
error: errMsg,
})
setError(_(msg`Incorrect username or password`))
} else if (isNetworkError(e)) {
logger.warn('Failed to login due to network error', {error: errMsg})
setError(
_(
msg`Unable to contact your service. Please check your Internet connection.`,
),
)
} else { } else {
logger.warn('Failed to login', {error: errMsg}) onAttemptFailed()
setError(cleanError(errMsg)) if (errMsg.includes('Token is invalid')) {
logger.debug('Failed to login due to invalid 2fa token', {
error: errMsg,
})
setError(_(msg`Invalid 2FA confirmation code.`))
} else if (
errMsg.includes('Authentication Required') ||
errMsg.includes('Invalid identifier or password')
) {
logger.debug('Failed to login due to invalid credentials', {
error: errMsg,
})
setError(_(msg`Incorrect username or password`))
} else if (isNetworkError(e)) {
logger.warn('Failed to login due to network error', {error: errMsg})
setError(
_(
msg`Unable to contact your service. Please check your Internet connection.`,
),
)
} else {
logger.warn('Failed to login', {error: errMsg})
setError(cleanError(errMsg))
}
} }
} }
} }
+4
View File
@@ -4,6 +4,7 @@ import {BskyAgent} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {isNetworkError} from '#/lib/strings/errors' import {isNetworkError} from '#/lib/strings/errors'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {checkAndFormatResetCode} from '#/lib/strings/password' import {checkAndFormatResetCode} from '#/lib/strings/password'
@@ -48,6 +49,7 @@ export const SetNewPasswordForm = ({
msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
), ),
) )
logEvent('signin:passwordResetFailure', {})
return return
} }
@@ -67,9 +69,11 @@ export const SetNewPasswordForm = ({
password, password,
}) })
onPasswordSet() onPasswordSet()
logEvent('signin:passwordResetSuccess', {})
} catch (e: any) { } catch (e: any) {
const errMsg = e.toString() const errMsg = e.toString()
logger.warn('Failed to set new password', {error: e}) logger.warn('Failed to set new password', {error: e})
logEvent('signin:passwordResetFailure', {})
setIsProcessing(false) setIsProcessing(false)
if (isNetworkError(e)) { if (isNetworkError(e)) {
setError( setError(
+30 -3
View File
@@ -1,10 +1,11 @@
import React from 'react' import React, {useRef} from 'react'
import {KeyboardAvoidingView} from 'react-native' import {KeyboardAvoidingView} from 'react-native'
import {LayoutAnimationConfig} from 'react-native-reanimated' import {LayoutAnimationConfig} from 'react-native-reanimated'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {DEFAULT_SERVICE} from '#/lib/constants' import {DEFAULT_SERVICE} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useServiceQuery} from '#/state/queries/service' import {useServiceQuery} from '#/state/queries/service'
import {SessionAccount, useSession} from '#/state/session' import {SessionAccount, useSession} from '#/state/session'
@@ -28,6 +29,8 @@ enum Forms {
export const Login = ({onPressBack}: {onPressBack: () => void}) => { export const Login = ({onPressBack}: {onPressBack: () => void}) => {
const {_} = useLingui() const {_} = useLingui()
const failedAttemptCountRef = useRef(0)
const startTimeRef = useRef(Date.now())
const {accounts} = useSession() const {accounts} = useSession()
const {requestedAccountSwitchTo} = useLoggedOutView() const {requestedAccountSwitchTo} = useLoggedOutView()
@@ -79,6 +82,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
logger.warn(`Failed to fetch service description for ${serviceUrl}`, { logger.warn(`Failed to fetch service description for ${serviceUrl}`, {
error: String(serviceError), error: String(serviceError),
}) })
logEvent('signin:hostingProviderFailedResolution', {})
} else { } else {
setError('') setError('')
} }
@@ -86,6 +90,27 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
const onPressForgotPassword = () => { const onPressForgotPassword = () => {
setCurrentForm(Forms.ForgotPassword) setCurrentForm(Forms.ForgotPassword)
logEvent('signin:forgotPasswordPressed', {})
}
const handlePressBack = () => {
onPressBack()
logEvent('signin:backPressed', {
failedAttemptsCount: failedAttemptCountRef.current,
})
}
const onAttemptSuccess = () => {
logEvent('signin:success', {
isUsingCustomProvider: serviceUrl !== DEFAULT_SERVICE,
timeTakenSeconds: Math.round((Date.now() - startTimeRef.current) / 1000),
failedAttemptsCount: failedAttemptCountRef.current,
})
setCurrentForm(Forms.Login)
}
const onAttemptFailed = () => {
failedAttemptCountRef.current += 1
} }
let content = null let content = null
@@ -103,9 +128,11 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
serviceDescription={serviceDescription} serviceDescription={serviceDescription}
initialHandle={initialHandle} initialHandle={initialHandle}
setError={setError} setError={setError}
onAttemptFailed={onAttemptFailed}
onAttemptSuccess={onAttemptSuccess}
setServiceUrl={setServiceUrl} setServiceUrl={setServiceUrl}
onPressBack={() => onPressBack={() =>
accounts.length ? gotoForm(Forms.ChooseAccount) : onPressBack() accounts.length ? gotoForm(Forms.ChooseAccount) : handlePressBack()
} }
onPressForgotPassword={onPressForgotPassword} onPressForgotPassword={onPressForgotPassword}
onPressRetryConnect={refetchService} onPressRetryConnect={refetchService}
@@ -118,7 +145,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
content = ( content = (
<ChooseAccountForm <ChooseAccountForm
onSelectAccount={onSelectAccount} onSelectAccount={onSelectAccount}
onPressBack={onPressBack} onPressBack={handlePressBack}
/> />
) )
break break
+42
View File
@@ -0,0 +1,42 @@
import {useMemo} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSession} from '#/state/session'
import {SearchScreenShell} from '#/view/screens/Search/Search'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileSearch'>
export const ProfileSearchScreen = ({route}: Props) => {
const {name, q: queryParam = ''} = route.params
const {_} = useLingui()
const {currentAccount} = useSession()
const {data: resolvedDid} = useResolveDidQuery(name)
const {data: profile} = useProfileQuery({did: resolvedDid})
const fixedParams = useMemo(
() => ({
from: profile?.handle ?? name,
}),
[profile?.handle, name],
)
return (
<SearchScreenShell
navButton="back"
inputPlaceholder={
profile
? currentAccount?.did === profile.did
? _(msg`Search my posts`)
: _(msg`Search @${profile.handle}'s posts`)
: _(msg`Search...`)
}
fixedParams={fixedParams}
queryParam={queryParam}
testID="searchPostsScreen"
/>
)
}
+4 -1
View File
@@ -1,8 +1,10 @@
import {useMemo} from 'react'
import {Platform} from 'react-native' import {Platform} from 'react-native'
import {setStringAsync} from 'expo-clipboard' import {setStringAsync} from 'expo-clipboard'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {Statsig} from 'statsig-react-native-expo'
import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info' import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info'
import {STATUS_PAGE_URL} from '#/lib/constants' import {STATUS_PAGE_URL} from '#/lib/constants'
@@ -20,6 +22,7 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'AboutSettings'>
export function AboutSettingsScreen({}: Props) { export function AboutSettingsScreen({}: Props) {
const {_} = useLingui() const {_} = useLingui()
const [devModeEnabled, setDevModeEnabled] = useDevModeEnabled() const [devModeEnabled, setDevModeEnabled] = useDevModeEnabled()
const stableID = useMemo(() => Statsig.getStableID(), [])
return ( return (
<Layout.Screen> <Layout.Screen>
@@ -79,7 +82,7 @@ export function AboutSettingsScreen({}: Props) {
}} }}
onPress={() => { onPress={() => {
setStringAsync( setStringAsync(
`Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}`, `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`,
) )
Toast.show(_(msg`Copied build version to clipboard`)) Toast.show(_(msg`Copied build version to clipboard`))
}}> }}>
+2 -2
View File
@@ -330,7 +330,7 @@ export function useSearchPopularFeedsMutation() {
if (moderationOpts) { if (moderationOpts) {
return res.data.feeds.filter(feed => { return res.data.feeds.filter(feed => {
const decision = moderateFeedGenerator(feed, moderationOpts) const decision = moderateFeedGenerator(feed, moderationOpts)
return !decision.ui('contentList').filter return !decision.ui('contentMedia').blur
}) })
} }
@@ -371,7 +371,7 @@ export function usePopularFeedsSearch({
select(data) { select(data) {
return data.filter(feed => { return data.filter(feed => {
const decision = moderateFeedGenerator(feed, moderationOpts!) const decision = moderateFeedGenerator(feed, moderationOpts!)
return !decision.ui('contentList').filter return !decision.ui('contentMedia').blur
}) })
}, },
}) })
+5 -1
View File
@@ -5,6 +5,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {BSKY_SERVICE} from '#/lib/constants' import {BSKY_SERVICE} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
@@ -39,7 +40,10 @@ export function ServerInputDialog({
setPreviousCustomAddress(result) setPreviousCustomAddress(result)
} }
} }
}, [onSelect]) logEvent('signin:hostingProviderPressed', {
hostingProviderDidChange: fixedOption !== BSKY_SERVICE,
})
}, [onSelect, fixedOption])
return ( return (
<Dialog.Outer <Dialog.Outer
+17
View File
@@ -2,10 +2,12 @@ import React, {memo} from 'react'
import {AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {HITSLOP_20} from '#/lib/constants' import {HITSLOP_20} from '#/lib/constants'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {NavigationProp} from '#/lib/routes/types'
import {shareText, shareUrl} from '#/lib/sharing' import {shareText, shareUrl} from '#/lib/sharing'
import {toShareUrl} from '#/lib/strings/url-helpers' import {toShareUrl} from '#/lib/strings/url-helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -26,6 +28,7 @@ import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle' import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
import { import {
@@ -48,6 +51,7 @@ let ProfileMenu = ({
const {openModal} = useModalControls() const {openModal} = useModalControls()
const reportDialogControl = useReportDialogControl() const reportDialogControl = useReportDialogControl()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const navigation = useNavigation<NavigationProp>()
const isSelf = currentAccount?.did === profile.did const isSelf = currentAccount?.did === profile.did
const isFollowing = profile.viewer?.following const isFollowing = profile.viewer?.following
const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy
@@ -177,6 +181,10 @@ let ProfileMenu = ({
shareText(profile.did) shareText(profile.did)
}, [profile.did]) }, [profile.did])
const onPressSearch = React.useCallback(() => {
navigation.navigate('ProfileSearch', {name: profile.handle})
}, [navigation, profile.handle])
return ( return (
<EventStopper onKeyDown={false}> <EventStopper onKeyDown={false}>
<Menu.Root> <Menu.Root>
@@ -215,6 +223,15 @@ let ProfileMenu = ({
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemIcon icon={Share} /> <Menu.ItemIcon icon={Share} />
</Menu.Item> </Menu.Item>
<Menu.Item
testID="profileHeaderDropdownSearchBtn"
label={_(msg`Search Posts`)}
onPress={onPressSearch}>
<Menu.ItemText>
<Trans>Search Posts</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={SearchIcon} />
</Menu.Item>
</Menu.Group> </Menu.Group>
{hasSession && ( {hasSession && (
+56 -15
View File
@@ -17,7 +17,7 @@ import {
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native' import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native'
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
import {createHitslop, HITSLOP_20} from '#/lib/constants' import {createHitslop, HITSLOP_20} from '#/lib/constants'
@@ -55,7 +55,7 @@ import {List} from '#/view/com/util/List'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
import {Explore} from '#/view/screens/Search/Explore' import {Explore} from '#/view/screens/Search/Explore'
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' import {makeSearchQuery, Params, parseSearchQuery} from '#/screens/Search/utils'
import { import {
atoms as a, atoms as a,
native, native,
@@ -420,7 +420,13 @@ function SearchLanguageDropdown({
) )
} }
function useQueryManager({initialQuery}: {initialQuery: string}) { function useQueryManager({
initialQuery,
fixedParams,
}: {
initialQuery: string
fixedParams?: Params
}) {
const {query, params: initialParams} = React.useMemo(() => { const {query, params: initialParams} = React.useMemo(() => {
return parseSearchQuery(initialQuery || '') return parseSearchQuery(initialQuery || '')
}, [initialQuery]) }, [initialQuery])
@@ -439,8 +445,9 @@ function useQueryManager({initialQuery}: {initialQuery: string}) {
...initialParams, ...initialParams,
// managed stuff // managed stuff
lang, lang,
...fixedParams,
}), }),
[lang, initialParams], [lang, initialParams, fixedParams],
) )
const handlers = React.useMemo( const handlers = React.useMemo(
() => ({ () => ({
@@ -589,16 +596,34 @@ SearchScreenInner = React.memo(SearchScreenInner)
export function SearchScreen( export function SearchScreen(
props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>, props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
) { ) {
const queryParam = props.route?.params?.q ?? ''
return <SearchScreenShell queryParam={queryParam} testID="searchScreen" />
}
export function SearchScreenShell({
queryParam,
testID,
fixedParams,
navButton = 'menu',
inputPlaceholder,
}: {
queryParam: string
testID: string
fixedParams?: Params
navButton?: 'back' | 'menu'
inputPlaceholder?: string
}) {
const t = useTheme() const t = useTheme()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const route = useRoute()
const textInput = React.useRef<TextInput>(null) const textInput = React.useRef<TextInput>(null)
const {_} = useLingui() const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {currentAccount} = useSession() const {currentAccount} = useSession()
// Query terms // Query terms
const queryParam = props.route?.params?.q ?? ''
const [searchText, setSearchText] = React.useState<string>(queryParam) const [searchText, setSearchText] = React.useState<string>(queryParam)
const {data: autocompleteData, isFetching: isAutocompleteFetching} = const {data: autocompleteData, isFetching: isAutocompleteFetching} =
useActorAutocompleteQuery(searchText, true) useActorAutocompleteQuery(searchText, true)
@@ -657,6 +682,7 @@ export function SearchScreen(
const {params, query, queryWithParams} = useQueryManager({ const {params, query, queryWithParams} = useQueryManager({
initialQuery: queryParam, initialQuery: queryParam,
fixedParams,
}) })
const showFilters = Boolean(queryWithParams && !showAutocomplete) const showFilters = Boolean(queryWithParams && !showAutocomplete)
@@ -697,13 +723,14 @@ export function SearchScreen(
updateSearchHistory(item) updateSearchHistory(item)
if (isWeb) { if (isWeb) {
navigation.push('Search', {q: item}) // @ts-expect-error route is not typesafe
navigation.push(route.name, {...route.params, q: item})
} else { } else {
textInput.current?.blur() textInput.current?.blur()
navigation.setParams({q: item}) navigation.setParams({q: item})
} }
}, },
[updateSearchHistory, navigation], [updateSearchHistory, navigation, route],
) )
const onPressCancelSearch = React.useCallback(() => { const onPressCancelSearch = React.useCallback(() => {
@@ -752,13 +779,18 @@ export function SearchScreen(
const onSoftReset = React.useCallback(() => { const onSoftReset = React.useCallback(() => {
if (isWeb) { if (isWeb) {
// Empty params resets the URL to be /search rather than /search?q= // Empty params resets the URL to be /search rather than /search?q=
navigation.replace('Search', {}) // eslint-disable-next-line @typescript-eslint/no-unused-vars
const {q: _q, ...parameters} = (route.params ?? {}) as {
[key: string]: string
}
// @ts-expect-error route is not typesafe
navigation.replace(route.name, parameters)
} else { } else {
setSearchText('') setSearchText('')
navigation.setParams({q: ''}) navigation.setParams({q: ''})
textInput.current?.focus() textInput.current?.focus()
} }
}, [navigation]) }, [navigation, route])
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -779,8 +811,10 @@ export function SearchScreen(
} }
}, [setShowAutocomplete]) }, [setShowAutocomplete])
const showHeader = !gtMobile || navButton !== 'menu'
return ( return (
<Layout.Screen testID="searchScreen"> <Layout.Screen testID={testID}>
<View <View
ref={headerRef} ref={headerRef}
onLayout={evt => { onLayout={evt => {
@@ -795,14 +829,18 @@ export function SearchScreen(
}), }),
]}> ]}>
<Layout.Center style={t.atoms.bg}> <Layout.Center style={t.atoms.bg}>
{!gtMobile && ( {showHeader && (
<View <View
// HACK: shift up search input. we can't remove the top padding // HACK: shift up search input. we can't remove the top padding
// on the search input because it messes up the layout animation // on the search input because it messes up the layout animation
// if we add it only when the header is hidden // if we add it only when the header is hidden
style={{marginBottom: tokens.space.xs * -1}}> style={{marginBottom: tokens.space.xs * -1}}>
<Layout.Header.Outer noBottomBorder> <Layout.Header.Outer noBottomBorder>
<Layout.Header.MenuButton /> {navButton === 'menu' ? (
<Layout.Header.MenuButton />
) : (
<Layout.Header.BackButton />
)}
<Layout.Header.Content align="left"> <Layout.Header.Content align="left">
<Layout.Header.TitleText> <Layout.Header.TitleText>
<Trans>Search</Trans> <Trans>Search</Trans>
@@ -830,7 +868,10 @@ export function SearchScreen(
onChangeText={onChangeText} onChangeText={onChangeText}
onClearText={onPressClearQuery} onClearText={onPressClearQuery}
onSubmitEditing={onSubmit} onSubmitEditing={onSubmit}
placeholder={_(msg`Search for posts, users, or feeds`)} placeholder={
inputPlaceholder ??
_(msg`Search for posts, users, or feeds`)
}
hitSlop={{...HITSLOP_20, top: 0}} hitSlop={{...HITSLOP_20, top: 0}}
/> />
</View> </View>
@@ -850,7 +891,7 @@ export function SearchScreen(
)} )}
</View> </View>
{showFilters && gtMobile && ( {showFilters && !showHeader && (
<View <View
style={[ style={[
a.flex_row, a.flex_row,
@@ -871,7 +912,7 @@ export function SearchScreen(
<View <View
style={{ style={{
display: showAutocomplete ? 'flex' : 'none', display: showAutocomplete && !fixedParams ? 'flex' : 'none',
flex: 1, flex: 1,
}}> }}>
{searchText.length > 0 ? ( {searchText.length > 0 ? (
+5 -5
View File
@@ -277,7 +277,7 @@
multiformats "^9.9.0" multiformats "^9.9.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/lexicon@^0.4.4", "@atproto/lexicon@^0.4.7": "@atproto/lexicon@^0.4.7":
version "0.4.7" version "0.4.7"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.7.tgz#f5d31615c21bcfd3e655f1e4f11a40a62fea9f86" resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.7.tgz#f5d31615c21bcfd3e655f1e4f11a40a62fea9f86"
integrity sha512-/x6h3tAiDNzSi4eXtC8ke65B7UzsagtlGRHmUD95698x5lBRpDnpizj0fZWTZVYed5qnOmz/ZEue+v3wDmO61g== integrity sha512-/x6h3tAiDNzSi4eXtC8ke65B7UzsagtlGRHmUD95698x5lBRpDnpizj0fZWTZVYed5qnOmz/ZEue+v3wDmO61g==
@@ -3386,10 +3386,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@bitdrift/react-native@^0.6.2": "@bitdrift/react-native@^0.6.8":
version "0.6.2" version "0.6.8"
resolved "https://registry.yarnpkg.com/@bitdrift/react-native/-/react-native-0.6.2.tgz#8e75d45a63fccad38b310fdea8069fa929cb97c3" resolved "https://registry.yarnpkg.com/@bitdrift/react-native/-/react-native-0.6.8.tgz#386495857bc81345de418750b5ca0e3c3b964f6c"
integrity sha512-4DIsZwAr9/Q1RI7lsnUphRoMuOuLWWESNXI759niSmU8XHTJISwwOQzUm7qWn7waBJGhxaq+jn+vlTV5Fai6zw== integrity sha512-ixjJTEfUz3GeQ7srxpoYpnOGVx+iDA/A8Y3CZe5cg+/b0d8xur8fBKFoRBiXXohzJnYq4W8MIWIhLAwm5sD9oA==
dependencies: dependencies:
"@expo/config-plugins" "^9.0.14" "@expo/config-plugins" "^9.0.14"
fast-json-stringify "^6.0.0" fast-json-stringify "^6.0.0"