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:
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 → Moderation → Interaction settings.
|
||||
Settings → Moderation → Interaction settings
|
||||
</Text>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
+9
-2
@@ -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,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
+1401
-1246
File diff suppressed because it is too large
Load Diff
+1423
-1807
File diff suppressed because it is too large
Load Diff
+1294
-3541
File diff suppressed because it is too large
Load Diff
+700
-606
File diff suppressed because it is too large
Load Diff
+1345
-2841
File diff suppressed because it is too large
Load Diff
+1323
-1234
File diff suppressed because it is too large
Load Diff
+3034
-3418
File diff suppressed because it is too large
Load Diff
+238
-220
File diff suppressed because it is too large
Load Diff
+1553
-2904
File diff suppressed because it is too large
Load Diff
+1702
-1417
File diff suppressed because it is too large
Load Diff
+1319
-1008
File diff suppressed because it is too large
Load Diff
+709
-615
File diff suppressed because it is too large
Load Diff
+1314
-1685
File diff suppressed because it is too large
Load Diff
+1359
-2715
File diff suppressed because it is too large
Load Diff
+1302
-2169
File diff suppressed because it is too large
Load Diff
+1578
-2490
File diff suppressed because it is too large
Load Diff
+646
-586
File diff suppressed because it is too large
Load Diff
+1299
-2810
File diff suppressed because it is too large
Load Diff
+591
-503
File diff suppressed because it is too large
Load Diff
+717
-623
File diff suppressed because it is too large
Load Diff
+1301
-1697
File diff suppressed because it is too large
Load Diff
+743
-649
File diff suppressed because it is too large
Load Diff
+1298
-2173
File diff suppressed because it is too large
Load Diff
+1339
-1184
File diff suppressed because it is too large
Load Diff
+1060
-878
File diff suppressed because it is too large
Load Diff
+1685
-3190
File diff suppressed because it is too large
Load Diff
+1329
-1725
File diff suppressed because it is too large
Load Diff
+1354
-1091
File diff suppressed because it is too large
Load Diff
+800
-777
File diff suppressed because it is too large
Load Diff
+1293
-2791
File diff suppressed because it is too large
Load Diff
+1289
-3339
File diff suppressed because it is too large
Load Diff
+1309
-1699
File diff suppressed because it is too large
Load Diff
+1404
-1240
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
@@ -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',
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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`))
|
||||
}}>
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user