Special-case 'me' values in search queries (#11043)

This commit is contained in:
DS Boyce
2026-07-02 09:35:57 -07:00
committed by GitHub
parent 40df0d773a
commit b0a40145e9
6 changed files with 45 additions and 39 deletions
+8 -26
View File
@@ -47,30 +47,12 @@ export function countLines(str: string | undefined): number {
return str.match(/\n/g)?.length ?? 0 return str.match(/\n/g)?.length ?? 0
} }
// Augments search query with additional syntax like `from:me` /**
export function augmentSearchQuery(query: string, {did}: {did?: string}) { * Normalizes a raw search query for the backend. The iOS keyboard inserts smart
// Don't do anything if there's no DID * quotes, but only straight quotes work for exact-phrase matching. Operators
if (!did) { * like `from:me` are passed through untouched - the backend resolves `me` to
return query * the viewer.
} */
export function augmentSearchQuery(query: string) {
// replace “smart quotes” with normal ones return query.replaceAll(/[“”]/g, '"')
// iOS keyboard will add fancy unicode quotes, but only normal ones work
query = query.replaceAll(/[“”]/g, '"')
// We don't want to replace substrings that are being "quoted" because those
// are exact string matches, so what we'll do here is to split them apart
// Even-indexed strings are unquoted, odd-indexed strings are quoted
const splits = query.split(/("(?:[^"\\]|\\.)*")/g)
return splits
.map((str, idx) => {
if (idx % 2 === 0) {
return str.replaceAll(/(^|\s)from:me(\s|$)/g, `$1${did}$2`)
}
return str
})
.join('')
} }
+5 -5
View File
@@ -319,18 +319,18 @@ let SearchScreenPostResults = ({
}): React.ReactNode => { }): React.ReactNode => {
const ax = useAnalytics() const ax = useAnalytics()
const {t: l} = useLingui() const {t: l} = useLingui()
const {currentAccount, hasSession} = useSession() const {hasSession} = useSession()
const [isPTR, setIsPTR] = useState(false) const [isPTR, setIsPTR] = useState(false)
const trackPostView = usePostViewTracking('SearchResults') const trackPostView = usePostViewTracking('SearchResults')
const searchV2Enabled = ax.features.enabled(ax.features.SearchV2Enable) const searchV2Enabled = ax.features.enabled(ax.features.SearchV2Enable)
const augmentedV2Query = useMemo(() => { const augmentedV2Query = useMemo(() => {
return augmentSearchQuery(query || '', {did: currentAccount?.did}) return augmentSearchQuery(query || '')
}, [query, currentAccount]) }, [query])
const augmentedV1Query = useMemo(() => { const augmentedV1Query = useMemo(() => {
return augmentSearchQuery(queryWithParams || '', {did: currentAccount?.did}) return augmentSearchQuery(queryWithParams || '')
}, [queryWithParams, currentAccount]) }, [queryWithParams])
/* /*
* Both hooks are called to keep hook order stable; `enabled` ensures only the * Both hooks are called to keep hook order stable; `enabled` ensures only the
@@ -73,6 +73,12 @@ describe(`AdvancedSearchDialog serialize/parse`, () => {
expect(state.until).toBe('2024-02-01') expect(state.until).toBe('2024-02-01')
}) })
it(`keeps from:me in the query box rather than lifting it into a row`, () => {
const state = parseAdvancedSearch('from:me', {})
expect(state.query).toBe('from:me')
expect(state.filters.find(f => f.field === 'authors')).toBeUndefined()
})
it(`merges a query-box operator with the matching filter param`, () => { it(`merges a query-box operator with the matching filter param`, () => {
const state = parseAdvancedSearch('hi from:bob', {author: 'alice'}) const state = parseAdvancedSearch('hi from:bob', {author: 'alice'})
expect(state.query).toBe('hi') expect(state.query).toBe('hi')
@@ -5,7 +5,6 @@ import {augmentSearchQuery} from '#/lib/strings/helpers'
import {codeToLanguageName} from '#/locale/helpers' import {codeToLanguageName} from '#/locale/helpers'
import {useLanguagePrefs} from '#/state/preferences/languages' import {useLanguagePrefs} from '#/state/preferences/languages'
import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2' import {useSearchPostsV2Query} from '#/state/queries/search-posts-v2'
import {useSession} from '#/state/session'
import {type SearchFilters} from '#/screens/Search/searchParams' import {type SearchFilters} from '#/screens/Search/searchParams'
import {Admonition} from '#/components/Admonition' import {Admonition} from '#/components/Admonition'
import {createStaticClick, InlineLinkText} from '#/components/Link' import {createStaticClick, InlineLinkText} from '#/components/Link'
@@ -34,12 +33,8 @@ export function DetectedLanguagesAdmonition({
onPressLanguage: (code: string) => void onPressLanguage: (code: string) => void
}) { }) {
const {appLanguage} = useLanguagePrefs() const {appLanguage} = useLanguagePrefs()
const {currentAccount} = useSession()
const augmentedQuery = useMemo( const augmentedQuery = useMemo(() => augmentSearchQuery(query || ''), [query])
() => augmentSearchQuery(query || '', {did: currentAccount?.did}),
[query, currentAccount],
)
const {data} = useSearchPostsV2Query({ const {data} = useSearchPostsV2Query({
query: augmentedQuery, query: augmentedQuery,
@@ -26,6 +26,23 @@ describe(`extractSearchPostsParams`, () => {
input: `cats to:alice`, input: `cats to:alice`,
output: {q: `cats`, mentions: `alice`}, output: {q: `cats`, mentions: `alice`},
}, },
// `me` is resolved by the backend, so the :me operators stay in q verbatim
// instead of being lifted into author/mentions.
{
name: `keeps from:me in q verbatim`,
input: `cats from:me`,
output: {q: `cats from:me`},
},
{
name: `keeps to:me in q verbatim`,
input: `cats to:me`,
output: {q: `cats to:me`},
},
{
name: `keeps mentions:me in q verbatim`,
input: `cats mentions:me`,
output: {q: `cats mentions:me`},
},
{ {
name: `accumulates multiple hashtags into tag[]`, name: `accumulates multiple hashtags into tag[]`,
input: `#cats #dogs`, input: `#cats #dogs`,
+8 -2
View File
@@ -106,11 +106,17 @@ export function extractSearchPostsParams(query: string): ExtractedSearchParams {
switch (op) { switch (op) {
case 'from': case 'from':
result.author ??= value /*
* `me` is resolved to the viewer by the backend, so leave it in the
* query text verbatim rather than lifting it into a structured param.
*/
if (value === 'me') remaining.push(token)
else result.author ??= value
break break
case 'mentions': case 'mentions':
case 'to': case 'to':
result.mentions ??= value if (value === 'me') remaining.push(token)
else result.mentions ??= value
break break
case 'domain': case 'domain':
result.domain ??= value result.domain ??= value