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:
label: "Adds and removes users on curatelists from the profile"
id: "bottomBarSearchBtn"
- tapOn: "Search"
- tapOn: "Search for posts, users, or feeds"
- inputText: "bob"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
+1 -1
View File
@@ -16,7 +16,7 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "profileCardButton"
- tapOn:
id: "profilePager-selector-4"
id: "profilePager-selector-5"
- tapOn: "alice-favs"
- tapOn: "Pin to Home"
- tapOn:
+1 -1
View File
@@ -15,7 +15,7 @@ appId: xyz.blueskyweb.app
id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarSearchBtn"
- tapOn: "Search"
- tapOn: "Search for posts, users, or feeds"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
+2 -2
View File
@@ -12,8 +12,8 @@ appId: xyz.blueskyweb.app
# Navigate to another user profile via autocomplete
- tapOn:
id: "bottomBarSearchBtn"
- assertVisible: "Search"
- tapOn: "Search"
- assertVisible: "Search for posts, users, or feeds"
- tapOn: "Search for posts, users, or feeds"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
+1
View File
@@ -242,6 +242,7 @@ module.exports = function (config) {
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
'./plugins/withAndroidNoJitpackPlugin.js',
'./plugins/withNoBundleCompression.js',
'./plugins/shareExtension/withShareExtensions.js',
'./plugins/notificationsExtension/withNotificationsExtension.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/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/feed/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
+1 -1
View File
@@ -57,7 +57,7 @@
},
"dependencies": {
"@atproto/api": "^0.14.0",
"@bitdrift/react-native": "^0.6.2",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@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 {router} from '#/routes'
import {Referrer} from '../modules/expo-bluesky-swiss-army'
import {ProfileSearchScreen} from './screens/Profile/ProfileSearch'
import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings'
import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
@@ -207,6 +208,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => ProfileListScreen}
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
name="PostThread"
getComponent={() => PostThreadScreen}
@@ -721,15 +729,16 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
linking={LINKING}
theme={theme}
onStateChange={() => {
const routeName = getCurrentRouteName()
if (routeName === 'Notifications') {
logEvent('router:navigate:notifications', {})
}
logEvent('lake:router:navigate', {
from: prevLoggedRouteName.current,
})
prevLoggedRouteName.current = getCurrentRouteName()
}}
onReady={() => {
attachRouteToLogEvents(getCurrentRouteName)
logModuleInitTime()
onReady()
logEvent('lake:router:navigate', {})
}}>
{children}
</NavigationContainer>
@@ -81,8 +81,9 @@ export function PostInteractionSettingsControlledDialog({
<Trans>
You can set default interaction settings in{' '}
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
Settings &rarr; Moderation &rarr; Interaction settings.
Settings &rarr; Moderation &rarr; Interaction settings
</Text>
.
</Trans>
</Text>
</View>
+9 -2
View File
@@ -1,23 +1,30 @@
import {init, SessionStrategy} from '@bitdrift/react-native'
import {Statsig} from 'statsig-react-native-expo'
export {debug, error, info, warn} from '@bitdrift/react-native'
import {initPromise} from './statsig/statsig'
export {debug, error, info, warn} from '@bitdrift/react-native'
const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
initPromise.then(() => {
let isEnabled = false
let isNetworkEnabled = false
try {
if (Statsig.checkGate('enable_bitdrift')) {
if (Statsig.checkGate('enable_bitdrift_v2')) {
isEnabled = true
}
if (Statsig.checkGate('enable_bitdrift_v2_networking')) {
isNetworkEnabled = true
}
} catch (e) {
// Statsig may complain about it being called too early.
}
if (isEnabled && BITDRIFT_API_KEY) {
init(BITDRIFT_API_KEY, SessionStrategy.Activity, {
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}
ProfileFollows: {name: string}
ProfileKnownFollowers: {name: string}
ProfileSearch: {name: string; q?: string}
ProfileList: {name: string; rkey: string}
PostThread: {name: string; rkey: string}
PostLikedBy: {name: string; rkey: string}
+19 -1
View File
@@ -30,7 +30,9 @@ export type LogEvents = {
secondsActive: number
}
'state:foreground': {}
'router:navigate:notifications': {}
'lake:router:navigate': {
from?: string
}
'deepLink:referrerReceived': {
to: string
referrer: string
@@ -49,6 +51,22 @@ export type LogEvents = {
}
'signup:captchaSuccess': {}
'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': {
selectedInterests: string[]
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',
ProfileFollows: '/profile/:name/follows',
ProfileKnownFollowers: '/profile/:name/known-followers',
ProfileSearch: '/profile/:name/search',
ProfileList: '/profile/:name/lists/:rkey',
PostThread: '/profile/:name/post/:rkey',
PostLikedBy: '/profile/:name/post/:rkey/liked-by',
+30 -22
View File
@@ -45,6 +45,8 @@ export const LoginForm = ({
onPressRetryConnect,
onPressBack,
onPressForgotPassword,
onAttemptSuccess,
onAttemptFailed,
}: {
error: string
serviceUrl: string
@@ -55,6 +57,8 @@ export const LoginForm = ({
onPressRetryConnect: () => void
onPressBack: () => void
onPressForgotPassword: () => void
onAttemptSuccess: () => void
onAttemptFailed: () => void
}) => {
const t = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
@@ -131,6 +135,7 @@ export const LoginForm = ({
},
'LoginForm',
)
onAttemptSuccess()
setShowLoggedOut(false)
setHasCheckedForStarterPack(true)
requestNotificationsPermission('Login')
@@ -142,29 +147,32 @@ export const LoginForm = ({
e instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError
) {
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 {
logger.warn('Failed to login', {error: errMsg})
setError(cleanError(errMsg))
onAttemptFailed()
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 {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {isNetworkError} from '#/lib/strings/errors'
import {cleanError} from '#/lib/strings/errors'
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.`,
),
)
logEvent('signin:passwordResetFailure', {})
return
}
@@ -67,9 +69,11 @@ export const SetNewPasswordForm = ({
password,
})
onPasswordSet()
logEvent('signin:passwordResetSuccess', {})
} catch (e: any) {
const errMsg = e.toString()
logger.warn('Failed to set new password', {error: e})
logEvent('signin:passwordResetFailure', {})
setIsProcessing(false)
if (isNetworkError(e)) {
setError(
+30 -3
View File
@@ -1,10 +1,11 @@
import React from 'react'
import React, {useRef} from 'react'
import {KeyboardAvoidingView} from 'react-native'
import {LayoutAnimationConfig} from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {DEFAULT_SERVICE} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useServiceQuery} from '#/state/queries/service'
import {SessionAccount, useSession} from '#/state/session'
@@ -28,6 +29,8 @@ enum Forms {
export const Login = ({onPressBack}: {onPressBack: () => void}) => {
const {_} = useLingui()
const failedAttemptCountRef = useRef(0)
const startTimeRef = useRef(Date.now())
const {accounts} = useSession()
const {requestedAccountSwitchTo} = useLoggedOutView()
@@ -79,6 +82,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
logger.warn(`Failed to fetch service description for ${serviceUrl}`, {
error: String(serviceError),
})
logEvent('signin:hostingProviderFailedResolution', {})
} else {
setError('')
}
@@ -86,6 +90,27 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
const onPressForgotPassword = () => {
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
@@ -103,9 +128,11 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
serviceDescription={serviceDescription}
initialHandle={initialHandle}
setError={setError}
onAttemptFailed={onAttemptFailed}
onAttemptSuccess={onAttemptSuccess}
setServiceUrl={setServiceUrl}
onPressBack={() =>
accounts.length ? gotoForm(Forms.ChooseAccount) : onPressBack()
accounts.length ? gotoForm(Forms.ChooseAccount) : handlePressBack()
}
onPressForgotPassword={onPressForgotPassword}
onPressRetryConnect={refetchService}
@@ -118,7 +145,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
content = (
<ChooseAccountForm
onSelectAccount={onSelectAccount}
onPressBack={onPressBack}
onPressBack={handlePressBack}
/>
)
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 {setStringAsync} from 'expo-clipboard'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {Statsig} from 'statsig-react-native-expo'
import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info'
import {STATUS_PAGE_URL} from '#/lib/constants'
@@ -20,6 +22,7 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'AboutSettings'>
export function AboutSettingsScreen({}: Props) {
const {_} = useLingui()
const [devModeEnabled, setDevModeEnabled] = useDevModeEnabled()
const stableID = useMemo(() => Statsig.getStableID(), [])
return (
<Layout.Screen>
@@ -79,7 +82,7 @@ export function AboutSettingsScreen({}: Props) {
}}
onPress={() => {
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`))
}}>
+2 -2
View File
@@ -330,7 +330,7 @@ export function useSearchPopularFeedsMutation() {
if (moderationOpts) {
return res.data.feeds.filter(feed => {
const decision = moderateFeedGenerator(feed, moderationOpts)
return !decision.ui('contentList').filter
return !decision.ui('contentMedia').blur
})
}
@@ -371,7 +371,7 @@ export function usePopularFeedsSearch({
select(data) {
return data.filter(feed => {
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 {BSKY_SERVICE} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import * as persisted from '#/state/persisted'
import {useSession} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
@@ -39,7 +40,10 @@ export function ServerInputDialog({
setPreviousCustomAddress(result)
}
}
}, [onSelect])
logEvent('signin:hostingProviderPressed', {
hostingProviderDidChange: fixedOption !== BSKY_SERVICE,
})
}, [onSelect, fixedOption])
return (
<Dialog.Outer
+17
View File
@@ -2,10 +2,12 @@ import React, {memo} from 'react'
import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {HITSLOP_20} from '#/lib/constants'
import {makeProfileLink} from '#/lib/routes/links'
import {NavigationProp} from '#/lib/routes/types'
import {shareText, shareUrl} from '#/lib/sharing'
import {toShareUrl} from '#/lib/strings/url-helpers'
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 {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
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 {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
import {
@@ -48,6 +51,7 @@ let ProfileMenu = ({
const {openModal} = useModalControls()
const reportDialogControl = useReportDialogControl()
const queryClient = useQueryClient()
const navigation = useNavigation<NavigationProp>()
const isSelf = currentAccount?.did === profile.did
const isFollowing = profile.viewer?.following
const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy
@@ -177,6 +181,10 @@ let ProfileMenu = ({
shareText(profile.did)
}, [profile.did])
const onPressSearch = React.useCallback(() => {
navigation.navigate('ProfileSearch', {name: profile.handle})
}, [navigation, profile.handle])
return (
<EventStopper onKeyDown={false}>
<Menu.Root>
@@ -215,6 +223,15 @@ let ProfileMenu = ({
</Menu.ItemText>
<Menu.ItemIcon icon={Share} />
</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>
{hasSession && (
+56 -15
View File
@@ -17,7 +17,7 @@ import {
} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
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 {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 {Explore} from '#/view/screens/Search/Explore'
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils'
import {makeSearchQuery, Params, parseSearchQuery} from '#/screens/Search/utils'
import {
atoms as a,
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(() => {
return parseSearchQuery(initialQuery || '')
}, [initialQuery])
@@ -439,8 +445,9 @@ function useQueryManager({initialQuery}: {initialQuery: string}) {
...initialParams,
// managed stuff
lang,
...fixedParams,
}),
[lang, initialParams],
[lang, initialParams, fixedParams],
)
const handlers = React.useMemo(
() => ({
@@ -589,16 +596,34 @@ SearchScreenInner = React.memo(SearchScreenInner)
export function SearchScreen(
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 {gtMobile} = useBreakpoints()
const navigation = useNavigation<NavigationProp>()
const route = useRoute()
const textInput = React.useRef<TextInput>(null)
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const {currentAccount} = useSession()
// Query terms
const queryParam = props.route?.params?.q ?? ''
const [searchText, setSearchText] = React.useState<string>(queryParam)
const {data: autocompleteData, isFetching: isAutocompleteFetching} =
useActorAutocompleteQuery(searchText, true)
@@ -657,6 +682,7 @@ export function SearchScreen(
const {params, query, queryWithParams} = useQueryManager({
initialQuery: queryParam,
fixedParams,
})
const showFilters = Boolean(queryWithParams && !showAutocomplete)
@@ -697,13 +723,14 @@ export function SearchScreen(
updateSearchHistory(item)
if (isWeb) {
navigation.push('Search', {q: item})
// @ts-expect-error route is not typesafe
navigation.push(route.name, {...route.params, q: item})
} else {
textInput.current?.blur()
navigation.setParams({q: item})
}
},
[updateSearchHistory, navigation],
[updateSearchHistory, navigation, route],
)
const onPressCancelSearch = React.useCallback(() => {
@@ -752,13 +779,18 @@ export function SearchScreen(
const onSoftReset = React.useCallback(() => {
if (isWeb) {
// 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 {
setSearchText('')
navigation.setParams({q: ''})
textInput.current?.focus()
}
}, [navigation])
}, [navigation, route])
useFocusEffect(
React.useCallback(() => {
@@ -779,8 +811,10 @@ export function SearchScreen(
}
}, [setShowAutocomplete])
const showHeader = !gtMobile || navButton !== 'menu'
return (
<Layout.Screen testID="searchScreen">
<Layout.Screen testID={testID}>
<View
ref={headerRef}
onLayout={evt => {
@@ -795,14 +829,18 @@ export function SearchScreen(
}),
]}>
<Layout.Center style={t.atoms.bg}>
{!gtMobile && (
{showHeader && (
<View
// HACK: shift up search input. we can't remove the top padding
// on the search input because it messes up the layout animation
// if we add it only when the header is hidden
style={{marginBottom: tokens.space.xs * -1}}>
<Layout.Header.Outer noBottomBorder>
<Layout.Header.MenuButton />
{navButton === 'menu' ? (
<Layout.Header.MenuButton />
) : (
<Layout.Header.BackButton />
)}
<Layout.Header.Content align="left">
<Layout.Header.TitleText>
<Trans>Search</Trans>
@@ -830,7 +868,10 @@ export function SearchScreen(
onChangeText={onChangeText}
onClearText={onPressClearQuery}
onSubmitEditing={onSubmit}
placeholder={_(msg`Search for posts, users, or feeds`)}
placeholder={
inputPlaceholder ??
_(msg`Search for posts, users, or feeds`)
}
hitSlop={{...HITSLOP_20, top: 0}}
/>
</View>
@@ -850,7 +891,7 @@ export function SearchScreen(
)}
</View>
{showFilters && gtMobile && (
{showFilters && !showHeader && (
<View
style={[
a.flex_row,
@@ -871,7 +912,7 @@ export function SearchScreen(
<View
style={{
display: showAutocomplete ? 'flex' : 'none',
display: showAutocomplete && !fixedParams ? 'flex' : 'none',
flex: 1,
}}>
{searchText.length > 0 ? (
+5 -5
View File
@@ -277,7 +277,7 @@
multiformats "^9.9.0"
zod "^3.23.8"
"@atproto/lexicon@^0.4.4", "@atproto/lexicon@^0.4.7":
"@atproto/lexicon@^0.4.7":
version "0.4.7"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.7.tgz#f5d31615c21bcfd3e655f1e4f11a40a62fea9f86"
integrity sha512-/x6h3tAiDNzSi4eXtC8ke65B7UzsagtlGRHmUD95698x5lBRpDnpizj0fZWTZVYed5qnOmz/ZEue+v3wDmO61g==
@@ -3386,10 +3386,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@bitdrift/react-native@^0.6.2":
version "0.6.2"
resolved "https://registry.yarnpkg.com/@bitdrift/react-native/-/react-native-0.6.2.tgz#8e75d45a63fccad38b310fdea8069fa929cb97c3"
integrity sha512-4DIsZwAr9/Q1RI7lsnUphRoMuOuLWWESNXI759niSmU8XHTJISwwOQzUm7qWn7waBJGhxaq+jn+vlTV5Fai6zw==
"@bitdrift/react-native@^0.6.8":
version "0.6.8"
resolved "https://registry.yarnpkg.com/@bitdrift/react-native/-/react-native-0.6.8.tgz#386495857bc81345de418750b5ca0e3c3b964f6c"
integrity sha512-ixjJTEfUz3GeQ7srxpoYpnOGVx+iDA/A8Y3CZe5cg+/b0d8xur8fBKFoRBiXXohzJnYq4W8MIWIhLAwm5sD9oA==
dependencies:
"@expo/config-plugins" "^9.0.14"
fast-json-stringify "^6.0.0"