Add fulltext search for posts and profiles (closes #340) (#342)

* Refactor mobile search screen

* Remove 'staleness' fetch trigger on search

* Implement a temporary fulltext search solution

* Add missing key from profile search result

* A few UI & UX improvements to the search suggestions

* Update web search suggestions

* Implement search in web build
This commit is contained in:
Paul Frazee 2023-03-21 17:58:50 -05:00 committed by GitHub
parent 48e18662f6
commit a7e3ce2585
16 changed files with 587 additions and 283 deletions

69
src/lib/api/search.ts Normal file
View file

@ -0,0 +1,69 @@
/**
* This is a temporary off-spec search endpoint
* TODO removeme when we land this in proto!
*/
import {AppBskyFeedPost} from '@atproto/api'
const PROFILES_ENDPOINT = 'https://search.bsky.social/search/profiles'
const POSTS_ENDPOINT = 'https://search.bsky.social/search/posts'
export interface ProfileSearchItem {
$type: string
avatar: {
cid: string
mimeType: string
}
banner: {
cid: string
mimeType: string
}
description: string | undefined
displayName: string | undefined
did: string
}
export interface PostSearchItem {
tid: string
cid: string
user: {
did: string
handle: string
}
post: AppBskyFeedPost.Record
}
export async function searchProfiles(
query: string,
): Promise<ProfileSearchItem[]> {
return await doFetch<ProfileSearchItem[]>(PROFILES_ENDPOINT, query)
}
export async function searchPosts(query: string): Promise<PostSearchItem[]> {
return await doFetch<PostSearchItem[]>(POSTS_ENDPOINT, query)
}
async function doFetch<T>(endpoint: string, query: string): Promise<T> {
const controller = new AbortController()
const to = setTimeout(() => controller.abort(), 15e3)
const uri = new URL(endpoint)
uri.searchParams.set('q', query)
const res = await fetch(String(uri), {
method: 'get',
headers: {
accept: 'application/json',
},
signal: controller.signal,
})
const resHeaders: Record<string, string> = {}
res.headers.forEach((value: string, key: string) => {
resHeaders[key] = value
})
let resBody = await res.json()
clearTimeout(to)
return resBody as unknown as T
}

View file

@ -32,24 +32,39 @@ export class Router {
}
function createRoute(pattern: string): Route {
let matcherReInternal = pattern.replace(
/:([\w]+)/g,
(_m, name) => `(?<${name}>[^/]+)`,
)
const pathParamNames: Set<string> = new Set()
let matcherReInternal = pattern.replace(/:([\w]+)/g, (_m, name) => {
pathParamNames.add(name)
return `(?<${name}>[^/]+)`
})
const matcherRe = new RegExp(`^${matcherReInternal}([?]|$)`, 'i')
return {
match(path: string) {
const res = matcherRe.exec(path)
const {pathname, searchParams} = new URL(path, 'http://throwaway.com')
const addedParams = Object.fromEntries(searchParams.entries())
const res = matcherRe.exec(pathname)
if (res) {
return {params: res.groups || {}}
return {params: Object.assign(addedParams, res.groups || {})}
}
return undefined
},
build(params: Record<string, string>) {
return pattern.replace(
const str = pattern.replace(
/:([\w]+)/g,
(_m, name) => params[name] || 'undefined',
)
let hasQp = false
const qp = new URLSearchParams()
for (const paramName in params) {
if (!pathParamNames.has(paramName)) {
qp.set(paramName, params[paramName])
hasQp = true
}
}
return str + (hasQp ? `?${qp.toString()}` : '')
},
}
}

View file

@ -23,7 +23,7 @@ export type HomeTabNavigatorParams = CommonNavigatorParams & {
}
export type SearchTabNavigatorParams = CommonNavigatorParams & {
Search: undefined
Search: {q?: string}
}
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
@ -32,7 +32,7 @@ export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
export type FlatNavigatorParams = CommonNavigatorParams & {
Home: undefined
Search: undefined
Search: {q?: string}
Notifications: undefined
}
@ -40,7 +40,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
HomeTab: undefined
Home: undefined
SearchTab: undefined
Search: undefined
Search: {q?: string}
NotificationsTab: undefined
Notifications: undefined
}